From ae29230f599f529ef4b1e14e204d3b47b4494b3b Mon Sep 17 00:00:00 2001 From: loki Date: Tue, 3 Dec 2019 20:23:33 +0100 Subject: [PATCH] Removed Git history due to personal info --- .gitignore | 8 + .gitmodules | 6 + CMakeLists.txt | 92 +++++ FindFFmpeg.cmake | 144 +++++++ Simple-Web-Server | 1 + assets/box.png | Bin 0 -> 53404 bytes assets/demoCA/cacert.pem | 22 + assets/demoCA/cakey.pem | 28 ++ audio.cpp | 104 +++++ audio.h | 17 + config.cpp | 27 ++ config.h | 34 ++ crypto.cpp | 231 +++++++++++ crypto.h | 64 +++ input.cpp | 142 +++++++ input.h | 16 + main.cpp | 28 ++ moonlight-common-c | 1 + nvhttp.cpp | 521 +++++++++++++++++++++++ nvhttp.h | 19 + platform/common.h | 40 ++ platform/linux.cpp | 314 ++++++++++++++ queue.h | 87 ++++ stream.cpp | 864 +++++++++++++++++++++++++++++++++++++++ stream.h | 19 + sunshine.conf | 29 ++ utility.h | 643 +++++++++++++++++++++++++++++ uuid.h | 50 +++ video.cpp | 176 ++++++++ video.h | 27 ++ 30 files changed, 3754 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 100644 FindFFmpeg.cmake create mode 160000 Simple-Web-Server create mode 100644 assets/box.png create mode 100644 assets/demoCA/cacert.pem create mode 100644 assets/demoCA/cakey.pem create mode 100644 audio.cpp create mode 100644 audio.h create mode 100644 config.cpp create mode 100644 config.h create mode 100644 crypto.cpp create mode 100644 crypto.h create mode 100644 input.cpp create mode 100644 input.h create mode 100644 main.cpp create mode 160000 moonlight-common-c create mode 100644 nvhttp.cpp create mode 100644 nvhttp.h create mode 100644 platform/common.h create mode 100644 platform/linux.cpp create mode 100644 queue.h create mode 100644 stream.cpp create mode 100644 stream.h create mode 100644 sunshine.conf create mode 100644 utility.h create mode 100644 uuid.h create mode 100644 video.cpp create mode 100644 video.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..c676cc4c --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +build +cmake-build-* +.DS_Store + +*.swp +*.kdev4 + +.idea diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..576591c2 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "moonlight-common-c"] + path = moonlight-common-c + url = git@github.com:moonlight-stream/moonlight-common-c.git +[submodule "Simple-Web-Server"] + path = Simple-Web-Server + url = git@github.com:loki-47-6F-64/Simple-Web-Server.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..2450bf5a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,92 @@ +cmake_minimum_required(VERSION 2.8) + +project(Sunshine) +# set up include-directories + +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}) +add_subdirectory(Simple-Web-Server) +add_subdirectory(moonlight-common-c/enet) +set(SUNSHINE_TARGET_FILES + moonlight-common-c/reedsolomon/rs.c + moonlight-common-c/reedsolomon/rs.h + moonlight-common-c/src/AudioStream.c + moonlight-common-c/src/ByteBuffer.c + moonlight-common-c/src/ByteBuffer.h + moonlight-common-c/src/Connection.c + moonlight-common-c/src/ControlStream.c + moonlight-common-c/src/FakeCallbacks.c + moonlight-common-c/src/Input.h + moonlight-common-c/src/InputStream.c + moonlight-common-c/src/Limelight.h + moonlight-common-c/src/Limelight-internal.h + moonlight-common-c/src/LinkedBlockingQueue.c + moonlight-common-c/src/LinkedBlockingQueue.h + moonlight-common-c/src/Misc.c + moonlight-common-c/src/Platform.c + moonlight-common-c/src/Platform.h + moonlight-common-c/src/PlatformSockets.c + moonlight-common-c/src/PlatformSockets.h + moonlight-common-c/src/PlatformThreads.h + moonlight-common-c/src/RtpFecQueue.c + moonlight-common-c/src/RtpFecQueue.h + moonlight-common-c/src/RtpReorderQueue.c + moonlight-common-c/src/RtpReorderQueue.h + moonlight-common-c/src/RtspConnection.c + moonlight-common-c/src/Rtsp.h + moonlight-common-c/src/RtspParser.c + moonlight-common-c/src/SdpGenerator.c + moonlight-common-c/src/SimpleStun.c + moonlight-common-c/src/VideoDepacketizer.c + moonlight-common-c/src/Video.h + moonlight-common-c/src/VideoStream.c + utility.h + uuid.h + config.h config.cpp + main.cpp crypto.cpp crypto.h nvhttp.cpp nvhttp.h stream.cpp stream.h video.cpp video.h queue.h input.cpp input.h audio.cpp audio.h platform/linux.cpp platform/common.h) + +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/Simple-Web-Server + ${CMAKE_CURRENT_SOURCE_DIR}/moonlight-common-c/enet/include + ${CMAKE_CURRENT_SOURCE_DIR}/moonlight-common-c/reedsolomon + ${X11_INCLUDE_DIR} + ${FFMPEG_INCLUDE_DIRS} +) + +find_package(Threads REQUIRED) +find_package(OpenSSL REQUIRED) +find_package(FFmpeg REQUIRED) +find_package(X11 REQUIRED) + +list(APPEND SUNSHINE_COMPILE_OPTIONS -fPIC -Wall -Wno-missing-braces -Wno-maybe-uninitialized) +string(TOUPPER ${CMAKE_BUILD_TYPE} BUILD_TYPE) +if("x${BUILD_TYPE}" STREQUAL "xDEBUG") + list(APPEND SUNSHINE_COMPILE_OPTIONS -O0 -pedantic -ggdb3) +elseif("x${BUILD_TYPE}" STREQUAL "xRELEASE") + add_definitions(-DNDEBUG) + list(APPEND SUNSHINE_COMPILE_OPTIONS -O3) +endif() + +list(APPEND SUNSHINE_EXTERNAL_LIBRARIES + ${CMAKE_THREAD_LIBS_INIT} + ${OPENSSL_LIBRARIES} + enet + Xfixes + Xtst + ${X11_LIBRARIES} + ${FFMPEG_LIBRARIES} + + #FIXME: libpulse is linux only + pulse + pulse-simple + + #libpulse should be found with package_find + opus) + +add_definitions(-DSUNSHINE_ASSETS_DIR="${CMAKE_CURRENT_SOURCE_DIR}/assets") +add_executable(sunshine ${SUNSHINE_TARGET_FILES}) +target_link_libraries(sunshine ${SUNSHINE_EXTERNAL_LIBRARIES}) +target_compile_definitions(sunshine PUBLIC ${SUNSHINE_DEFINITIONS}) +set_target_properties(sunshine PROPERTIES CXX_STANDARD 17) + +target_compile_options(sunshine PRIVATE ${SUNSHINE_COMPILE_OPTIONS}) diff --git a/FindFFmpeg.cmake b/FindFFmpeg.cmake new file mode 100644 index 00000000..25070e40 --- /dev/null +++ b/FindFFmpeg.cmake @@ -0,0 +1,144 @@ +# - Try to find FFMPEG +# Once done this will define +# FFMPEG_FOUND - System has FFMPEG +# FFMPEG_INCLUDE_DIRS - The FFMPEG include directories +# FFMPEG_LIBRARIES - The libraries needed to use FFMPEG +# FFMPEG_LIBRARY_DIRS - The directory to find FFMPEG libraries +# +# written by Roy Shilkrot 2013 http://www.morethantechnical.com/ +# + +find_package(PkgConfig) + + +MACRO(FFMPEG_FIND varname shortname headername) + + IF(NOT WIN32) + PKG_CHECK_MODULES(PC_${varname} ${shortname}) + + FIND_PATH(${varname}_INCLUDE_DIR "${shortname}/${headername}" + HINTS ${PC_${varname}_INCLUDEDIR} ${PC_${varname}_INCLUDE_DIRS} + NO_DEFAULT_PATH + ) + ELSE() + FIND_PATH(${varname}_INCLUDE_DIR "${shortname}/${headername}") + ENDIF() + + IF(${varname}_INCLUDE_DIR STREQUAL "${varname}_INCLUDE_DIR-NOTFOUND") + message(STATUS "look for newer strcture") + IF(NOT WIN32) + PKG_CHECK_MODULES(PC_${varname} "lib${shortname}") + + FIND_PATH(${varname}_INCLUDE_DIR "lib${shortname}/${headername}" + HINTS ${PC_${varname}_INCLUDEDIR} ${PC_${varname}_INCLUDE_DIRS} + NO_DEFAULT_PATH + ) + ELSE() + FIND_PATH(${varname}_INCLUDE_DIR "lib${shortname}/${headername}") + IF(${${varname}_INCLUDE_DIR} STREQUAL "${varname}_INCLUDE_DIR-NOTFOUND") + #Desperate times call for desperate measures + MESSAGE(STATUS "globbing...") + FILE(GLOB_RECURSE ${varname}_INCLUDE_DIR "/ffmpeg*/${headername}") + MESSAGE(STATUS "found: ${${varname}_INCLUDE_DIR}") + IF(${varname}_INCLUDE_DIR) + GET_FILENAME_COMPONENT(${varname}_INCLUDE_DIR "${${varname}_INCLUDE_DIR}" PATH) + GET_FILENAME_COMPONENT(${varname}_INCLUDE_DIR "${${varname}_INCLUDE_DIR}" PATH) + ELSE() + SET(${varname}_INCLUDE_DIR "${varname}_INCLUDE_DIR-NOTFOUND") + ENDIF() + ENDIF() + ENDIF() + ENDIF() + + + IF(${${varname}_INCLUDE_DIR} STREQUAL "${varname}_INCLUDE_DIR-NOTFOUND") + MESSAGE(STATUS "Can't find includes for ${shortname}...") + ELSE() + MESSAGE(STATUS "Found ${shortname} include dirs: ${${varname}_INCLUDE_DIR}") + +# GET_DIRECTORY_PROPERTY(FFMPEG_PARENT DIRECTORY ${${varname}_INCLUDE_DIR} PARENT_DIRECTORY) + GET_FILENAME_COMPONENT(FFMPEG_PARENT ${${varname}_INCLUDE_DIR} PATH) + MESSAGE(STATUS "Using FFMpeg dir parent as hint: ${FFMPEG_PARENT}") + + IF(NOT WIN32) + FIND_LIBRARY(${varname}_LIBRARIES NAMES ${shortname} + HINTS ${PC_${varname}_LIBDIR} ${PC_${varname}_LIBRARY_DIR} ${FFMPEG_PARENT}) + ELSE() +# FIND_PATH(${varname}_LIBRARIES "${shortname}.dll.a" HINTS ${FFMPEG_PARENT}) + FILE(GLOB_RECURSE ${varname}_LIBRARIES "${FFMPEG_PARENT}/*${shortname}.lib") + # GLOBing is very bad... but windows sux, this is the only thing that works + ENDIF() + + IF(${varname}_LIBRARIES STREQUAL "${varname}_LIBRARIES-NOTFOUND") + MESSAGE(STATUS "look for newer structure for library") + FIND_LIBRARY(${varname}_LIBRARIES NAMES lib${shortname} + HINTS ${PC_${varname}_LIBDIR} ${PC_${varname}_LIBRARY_DIR} ${FFMPEG_PARENT}) + ENDIF() + + + IF(${varname}_LIBRARIES STREQUAL "${varname}_LIBRARIES-NOTFOUND") + MESSAGE(STATUS "Can't find lib for ${shortname}...") + ELSE() + MESSAGE(STATUS "Found ${shortname} libs: ${${varname}_LIBRARIES}") + ENDIF() + + + IF(NOT ${varname}_INCLUDE_DIR STREQUAL "${varname}_INCLUDE_DIR-NOTFOUND" + AND NOT ${varname}_LIBRARIES STREQUAL ${varname}_LIBRARIES-NOTFOUND) + + MESSAGE(STATUS "found ${shortname}: include ${${varname}_INCLUDE_DIR} lib ${${varname}_LIBRARIES}") + SET(FFMPEG_${varname}_FOUND 1) + SET(FFMPEG_${varname}_INCLUDE_DIRS ${${varname}_INCLUDE_DIR}) + SET(FFMPEG_${varname}_LIBS ${${varname}_LIBRARIES}) + ELSE() + MESSAGE(STATUS "Can't find ${shortname}") + ENDIF() + + ENDIF() + +ENDMACRO(FFMPEG_FIND) + +FFMPEG_FIND(LIBAVFORMAT avformat avformat.h) +FFMPEG_FIND(LIBAVDEVICE avdevice avdevice.h) +FFMPEG_FIND(LIBAVCODEC avcodec avcodec.h) +FFMPEG_FIND(LIBAVUTIL avutil avutil.h) +FFMPEG_FIND(LIBSWSCALE swscale swscale.h) + +SET(FFMPEG_FOUND "NO") +IF (FFMPEG_LIBAVFORMAT_FOUND AND + FFMPEG_LIBAVDEVICE_FOUND AND + FFMPEG_LIBAVCODEC_FOUND AND + FFMPEG_LIBAVUTIL_FOUND AND + FFMPEG_LIBSWSCALE_FOUND + ) + + + SET(FFMPEG_FOUND "YES") + + SET(FFMPEG_INCLUDE_DIRS ${FFMPEG_LIBAVFORMAT_INCLUDE_DIRS}) + + SET(FFMPEG_LIBRARY_DIRS ${FFMPEG_LIBAVFORMAT_LIBRARY_DIRS}) + + SET(FFMPEG_LIBRARIES + ${FFMPEG_LIBAVFORMAT_LIBS} + ${FFMPEG_LIBAVDEVICE_LIBS} + ${FFMPEG_LIBAVCODEC_LIBS} + ${FFMPEG_LIBAVUTIL_LIBS} + ${FFMPEG_LIBSWSCALE_LIBS} + ) + +ELSE () + + MESSAGE(STATUS "Could not find FFMPEG") + +ENDIF() + +message(STATUS ${FFMPEG_LIBRARIES} ${FFMPEG_LIBAVFORMAT_LIBRARIES}) + +include(FindPackageHandleStandardArgs) +# handle the QUIETLY and REQUIRED arguments and set FFMPEG_FOUND to TRUE +# if all listed variables are TRUE +find_package_handle_standard_args(FFMPEG DEFAULT_MSG + FFMPEG_LIBRARIES FFMPEG_INCLUDE_DIRS) + +mark_as_advanced(FFMPEG_INCLUDE_DIRS FFMPEG_LIBRARY_DIRS FFMPEG_LIBRARIES) diff --git a/Simple-Web-Server b/Simple-Web-Server new file mode 160000 index 00000000..d1bf544d --- /dev/null +++ b/Simple-Web-Server @@ -0,0 +1 @@ +Subproject commit d1bf544d9266bdf5b86517e4a70de63216529e5b diff --git a/assets/box.png b/assets/box.png new file mode 100644 index 0000000000000000000000000000000000000000..b3f6c0ad168f1e1f56a722985ff04f9886fe8d18 GIT binary patch literal 53404 zcmZsE2UyO1`~Ei}qCCiIp`krQ#EnQarM;vnD$&p&ql~hnZDh134WZplXqgQ{si&+GQQ@9+0Jj{o}}@8NlF_x=5T#`U?b^E$8d`n<=DjdTR~h4~qV5zy1sHf0#z zM*L5H!gze*VmR?5{usM$&4x7$lXqs)Z>Mqicb;9QI$F&2$D%#>mw7I_rW+V0XaU27 z9ATKB_$cH9!}zN*Os^xuEI-FEv%KSrO;+G9#ycD6Xfvbq->YS5QTSw{x30}D{He?R z-};*EtMGG1PkW8|?pOV-fqoViBO{-l=#A5I{zUzDW``P+(JPZ>aapg+Rb^9`TtLt`Cr$)rbix&C33p;kKrO;=s zudlDRwziyfnA5S6l9Fgy6D@7+6GCzl>rT#`F?FhggM*w+e)8@;d*&368Zu0w{FGKa z&^utK2hvQPRtn**uP4Ch6|(9(nn)a!*fB#h>>D1&fRJ{m9=i`m(&dy!OeH zjkPffG6xPEFumnz^Xc>F>6)6F8O8~R%yaBFXIf+};NjtU+tZ`>V?4tSn;ekN!whfx z@#9DDhi6Iofdlr-g1)3n$;i|lpDrVXmzkQH9(ZlDL?JLRFvC0}(mcy@L#9=Za#NO7 zob62yy~E=Lq*PVUUfRhpoH3!oOo(QoZ`X-e6~Pr!($X~tdHJLkE;I=kc=PA=GtaC~ zgM(*7LYRq@Cr4ksdbLeIz|ryQ?65V@#xw0I#u$gPl8Va4k8jF0O`A4NYSk)x{L%TP zr=i&8yw-U&*JS^`=hH+*6%!H?>J!&ZlCv$49VfzsIGE5cyI$SgEyjNed&eL*sc9vSQqn9*t*}Hy7*f-d$MW-Viq6%*)vEV^)sK=FFMnQ5hzX?|h&CM^D-njk_P`ty;CJ z{`zLt#3@taj4mzLi;=gGeZepWNyfa4Vn<8SuHIh*6>->DJuR(6o$ufGg?A2JSh7QY zWzd(i_GLkOk1nXo;rn$7_!Zs;b68-}>eD z|N0h#37TE!VPaP&jAt}r6s)eN4^)iW&s4UP607vYSf+X25i6@%;i>I0LilM~^J(VwIvsf4|op z`us$*FPwLJy5+SCljo>uhfSU%A#D(wY>h!2)e>Ny_Wu0Z=kYw-W(W`0*VjM6(NIm8 zCaquHuu)Sfr~GH8`E|SV&H{|80KYo3`s8#OqjZ(>VXf4SSCnz~o{8}pvFqx%i$Pyr z?Ad%4Uw#^nCwr%Nc5eCl?X_23>7LKKy%uv$IL)3WF0SlV@#og!oh|3+dtvmwV-0B= zv!<*XUX6!7KK#IOdYF^$`^T4D?;e?0pR6~X)}e#uRZ_hBQ{sgcd-4r0Ew{+e&-bf3 zIIgIuNbftRwd+#gn~3H9y~f!*jNn@AUh}%3pyCS}K8gFjz1$Xg{(J^Y&@SZY(WBby z*Vk_HDwDRzvP{7e^wU6dGfGQWsBeF+IA_t;Q^_eQJ>k5wI=i~0*M?1w2z;|gXW#z) za?kO4+BnYz3l^*|`%-U^A3S=X)r85g&PzNc##G|KVHnKh3Ph8GgZ=zF4c>TE8KxyjQ;els_vvpdG$hXW_1F+ zH?Z^fkI%hLRt_;myWaPPk7>Di_Uu1DhU!#O4*dBU?ZL1&zpGx#wBM3VX^daC%N(vn zw!u5-)2C1M**1&jEZrGjTT|l{$isMBHDeWjC%vmWlXd4XpMjy_DUNK&BX!Sap?KkCA1iNVh0ZW3UIb!es|TRy(Jnd$THL9}~eycjD$H)_F#ExnBy z{Td82G8HFn@M3nhqWZpJ@1{H#on?E!Wpgw_W-j%56FpUOy)KNwA}DD2eS9rWV@QDc zXKF;*-tU<*mv?JLMn?MS@E9ifcGao{a_siBALP>*BghO-C;zM#JhHE;*nfNN;z7Q& zjaOoGb5&_MO#8L))(|ld#@lJno;|&%m%cfx7qtMRKWG2q`es+e3Df*7dI4EHjD!|e zEY82tY)}jpvxIUmu2eR=qu^ER~<1U-miEewQYu zETO6CK$bZ4scLRt_!#R4E@xIQXgl!hnvjC!Dam%G{SXdK`?iW|dhv4_ji@pD$)#-=0+A|GeB$`N_)f@%!-Ax82&MkJ24C)A- zCEE}(asSAV_d|_l8};n%#h1!5){`;a_Ra^V<|K{$83^tRKgcpq)}1Q1=~lLoioM;w zefzYv?^RXll$Ms_A5gEap5K9?u(!*Q9*XK@%$ zl$5YoO^oAIaXR>%H>`ouwda@2S-Ny7`H=tM28#K;O<4x_Ps~WMEAoB&{yl+=x9{GW zrJKfFx^%6+zFunO%FXnAfEA;7jrlb-HNJ1}ObPLc_^3(pub{lY?* zFk!VV6H=-V65Lv|hJb|#dv@>E_4dy7>u)xBb!&$Lp8odZ z#~n3eq5%ZI4YaA`<>gUOe_800v}Nno-f-7F1ErtC5sn{3z7S#x`C(WZ5w3Ny8JAb? z9lGq!q5&1`cx_qi*CWx<(P3Kb=N_lXvazv|2ecu)0zZ#{?yr-dO`&OhxM)x0i#d!n z53D%;9%7~i{4g+_PN0whfwnLGFU=$*B$5$W?p7v_^tazR?9p*IG~ex+#IoIkS2-H7 z&a=bzeSWlb+MGEx{(OwlF+?7td2(`kp+}BX18ex{6@G0@gNg5!=bajdYY$Ykrz zhVuQt8WY!@>?|25k4n-x(;MzG5j#Yab%%?1F`v-ci5#*{7kcS`p&yN8%e*5-qGHk#uj%nAXljXd0 zXN&Qa6@wxD&3S7Bh0-h%w8rg03{43h9U)XwdqONZEiG%=uI}nMhXXYcgIa_vJYVLo z;}pFYzi!5OPlHEM%{X*uOjDB16oM>%IzIZRh1DYy)*jVISQC;nJ#y&nJRct)_&~jU z-!6UozJOVW4jtOkkh%eeQK>$OF$%%zEFd4RzT?&6)`js#SdUY}>iXEOySxt;zbXmL z`T3<`Bl+LRn3xn+xLHq2(T(53@6NpCa2|i1#4OOkDWComPM~qula*<#um;u1Yl{O1 zl)cKn&TLzwex5+b_qJe%tq;%5fBEud@jH#v5t2H!d~^4)Z%+RE^Qf1_{#ii?6S8w} z%PA@vE-;Fhh2cd{na=X>jsE#0ctjFUdH!=zct(Z`NWxW1iHT}Ycz{p0g{3uEK6 zh_UMfg&O*vCLA_Ar@F?rz->*3w*<~m@5kG1vG5A|@EjbCRLj!cKJ>E1ZcjtDi!z2M zX!;RxacOt&-lb(JPC94XTI}yS)LBzsv}>Dw*mRcPhqFOFDREoA_BPSqB4)b&`u356 z(e*$^b^cPyYDTwpR!2nlRG7fJ&jSqM5XY(Q;K5H8SzfbsRdgt~1N6&ag}aL_^VNn? z%bXDVhf$TLDX`&%8yhps{Hs)@fiCAP_e&RD8Dzfxw6H6ZCzDhqsTC_)G`!nGO25C^ zi)~9{h0Cp5x2|UIKv^0K*~Ux?QWq!#xas-Ql%f#pL{AI;R&7_eq5I2xY3WzvaDKAp zuMMjP?)KMND2CVAlm*>95*q5G!h{@0P+6VS-|yL!?`rV!#N zLu%WT%S~eUg(xQk+KB=h(;qW}ylov~>qci67tA}<`Thw#XXnJmEUPoW#xaWtp}DC# zvYFa1IkYExzjIOy->HlnJ9akwXB?8o6G*u@8n0ZpZq*0Yl9rJh#>q~*CQh8#`%GtU4A(#k z28V_sF=Kro-@*!peTTpR-)`*Q=39ope*Ub53zlohz1ZIF=zEowQ#~?>FNp-Ti>CvQy&?bQ_x;V)# zZ^!E;Kl*Px@vCB(xXK7GCO3(~;CKPy>I}0qILAUZd-d_~@K~&qX=;b+`Sa&9b8#`dd3TaWolkhWlgtJzX>66dHECDu{sDnx!UO0<*FZ8PgstoIhp& zfY_EfAnVMV8PB|lj=GukEWPsn6^YQ zj+8cpoZfI{RclJEAFR6*$w;DMWx?c1RmxMRPMwOkJq(>Rs}pJBGnkFy836j+1WNGX zdJJrBFK}c9O)3uTe_7xj6|GS5sqhX63hYb)}|t$Z!c`+}!@6dt@xbqCN0* zgD+M#OT{x3pFMjP?XDr9qN3tb6Ddt_2dgLnZ15(iPpNew!>%VQ6Vcna(TeaLcE2y& zIS@;dzvuH-_m;wiuEIt#N=h5X!9gJg0SU0^WEJg_`7X~iPbBayi6qGPxY|b!M;Q=b zX_BADHLS33mgTibq%%Eu9QdmtB-?%A;4khYD{E#WyqJRlkzTaOEKsQA7s6#19A%ne zWzUW`r8X`>}7J0Aw>{w#6acO|BIHoc5@pzQl)gXkWN#WdAv4x(hmLOAW# zh5bfy(#Qy3&=MZU63QmS9{}+v747+)%8?CdD&N1?2FG~!?%l@ePI8q;r&u9)HHcMm z=r|yEcsnwV5pl-y@w+3tk5y>^Pc{T&#;MU8{qZDNYVl(8z{w?hfs_1oK-|zu^LHSn z%>)s~4fH2kfgiNtIDd>tD}Kb^$ji#=0tFB1jg_FSfIAu-OHd%#>u~IQupu3u4|Gv(HeBzF?CMmxM2}en`nhZOYE2gmWLLfqLxQtL3OpRI&xq9MWR+HeO@6tuNvYAW9y_ zFN;zWI99DQcfV9uUEB+?O6A9wS1JJLkl&3L2Mp$%jX86u9BV)CCE3}bYKAa7YAEP8%P|t(+tT&Bl0|$iCm>>Cy;am41D1f zhAl>?V`n8ioRSsncfrQXhUdp~*t8AU!eW=^XjA>}Cs%Ud*)<5KwS)op_k}y|LZUU) z>~h8=^&rbQ$9|;?JA?<{Zn+yei94cu2t31lFLE5L0F8VTb}2;W1)k>`mLwCSik7#~H@dP)39L89AZUBbGlNKWnCM*~ALRV!RhafAjbeAUf=_V;hfX2Y493h>%C;*q4IF!sSU1jukd&{S z)=m1+vI*Eb?|uYrg_o;cB0KhKVvVaNJrVO@G46`0TlxM$Cp?# z@M_PkTX~HV5uH3<-`Y{^B6PuNwlAk;84~{$yCyKd#{t>22V$R{kE9BpYYnyBbQ%-7_LACOvNV-2=HOAG~aF~Ud9 zplc|){rxHx7j$xu7Qm-dMD-x*Q2xD*a;*#bKf(Bb~L6l@42<&OhAa<=hw1Wupgv$st7t) zS?d#Qy4LD2x!e(`^ZO%1xYycxi7%38K|{KUWb0J^NBp~_m}Xv#=ALkgilub{uVrJd z(^~w^#|NiHd_TQi_vR))6AVYyKD6^qsrzxOtu{{21g#@1R`ol_!QhdhvAdA2oiu%1 z@yR)69m`SOvS_Is)_5B>LHP-PRoOaixScfCn2=3_pFb}`l_(7ZBX{)#W4E*S@W|v$ z2(<(qN!3QqY-U6Y*@mBv>|%w7F^nVarLqd3WZ)64EZduz02!Z-c(IdFPQ1?2N=Qjd z>kDU?R8&aXhq7~Z3+{4eIl5ZPHd+o3PUWs5yUQlQc#~6}p!N9~tG?vlwVTM?LVTrf z_c@2&Uyg4NKds~6CCxMohYE+(-MA}}XPTl<>n&ngsB=x#YD~yA47YHFf~d=v&z~E_ zc-U>ZcUK0zdmvC#TWeapiwQZ8#2$a&UmUHp$r9TboP_h{Jp1wJyaNjR7`#xMUdUyL zwGv|F%+}L)DjOOaki#~KB|Sy;FqwP^s)iv3b*OcLs>J!WE%NQcff<~V?cL9!G-oWM zIs?`e7aJ9ogvsv-3OX?(pq$CNao1`Q*>wf2=!F^l{-6-<=iZH5#B)-?zO)eQyESy# zbjI^Ib}Plk>GplVe?08=9sci`GrA3#T#CDKTSHrCDk#?TShaurcny!Yae6$K*_3F^ z>+No7Eo$o&Ay_y@p(Tp~=;(hw&0i~^!G{kmhyQ`ma-ZMT-Q62*>}F}5%zK?8Vy-wI zPX7r!`1kcy$0W~%?8?2%x&FW3{jMGnVwrV^{mr}a|E^elcCJRj-Rra3`Ln%q@Fr&T zN;F558F|A6 zE@WEg8Ak&$tw;PNN+^T)e(kjY+WdX^%NM`D=VHOX57-qLxOBzJmHt)um0}}LR*7Fv zBo>q)+UPpkR%D#8)&fe#r!v1!!?U@DO14>^Q{iF9(tcc?G4mMKtb1$B-bVh&je@D2@%ownc!U7U=`t;-m;CsuggbJ<2za?YaWdHIXLxl=RvZ_en3 zRq&DHRiAkh5y*FV(22I!$lm^P{#{;P1P$gkwvfqt?Cb-Ind@B<6m++4P36oEwM<$s z@L%sL0h@U)c4q+?LEz3omSvI5cA^-uOxQ_Ao5*H5>!o5@if?$bj-5Q&fNujwagQ|j zEB9W!c#(TIZ$MHfyc$XSZq+%C(>C4md`90};+g)RM;*Wl&rXSUojDkC!@$VM5>>nB zSP^-POex^G$H7PVn0}&>EAndvAEs`&2;VSI0%ZaU*B0ID|IYY#EkCPKLIOj4hR}A5 zI1%nrwoG8Y5#V&Naea0!I$BC*0KYzdt|WW|LtXth*}z`=-{`*4$d7hZ{BjV6N@;*+U7jw{PDD>xVebs48Lo ze;c910eqNu;7?G^mh z$>T-)Q74jz1_i5mQgFM6Z#?}qxn(%{U1-GAOn8^SgY(;XOzx{=B z_@N=w0Wi|}GlIwc`#qmsHy#V2#DR>GF}SE1Ty9r!U^CvFSAX;b;BYBz z{UM7t(h3s9x8sYahKGl}k4(?yRd_UO-3g%x_(fR6?9bzv-@JXE6->x!e-y4@Sw%r# zGap`1H^7u4L1HMGA;&|~ZEzkbDZ*Yzh2u@hGjsQUiGTpXM{#0qUQHC7m!p~G#a|5j zGVSLqqy=)Wwb4j{wnD7bb#(3l5D&T0AX~EF6h4GInPFWD_tWA`;&3=&O*3OHU+eMu z{Ti&Cl5+LxYRn_UA}ba=+|hS$81IYp(4|noWWYHA;%z`hNaLG8GaDt0Ce$xrPZ=m} zkUeD}aJPF=2~Iy&F#`!p*`IH(;S6F2M}7?$SX##71W@h!f4;kW?_QBJtA3{AFJ_2| zDS-tB4w5}`W80HVQjuUNKqi+-H9|w-qoGW}&ZV#pJu3V3Km^Y=`q(x)a+_Pyb295U z;$>f26zv=}h8ZzJrL*|`<-MKtDF#T<%%M;q1m3@%Cr}opQD9vIV6Y6F z&a?|s@-)f@me}2&SB0q+Ep6LphNB@zQ;Ah*5D;0CYzR(>a;F`+Z}SF}Hr=X5;`?Nw zbi1JFirvN_)UbIM-xV%pTExJXvv*Y2)tTXKQH!=c{t)hLMhstavJ{H16If>K9fZ$q zO9HN81rX{NfY^1LSjoQoZfIAjZkndo30z4JO-d{lFHMr zoJU!o9O?#5=_YH3K7aNHq44(pNiHXa`hjBl!HToMIdG7&Bq;#VavP<`o|iXdzH2fq z++=47RS=w)e^ZnLu18(6uwlUwUZ55;?PCf#^ zjUbk=FgK6p%npcvI|YmU{xV-k$vxspc&1F5lH^cv0OMNc>zhx7G8_+cc)V>d)ml?G zh}14-gr>picps%i8NglPZ(1IYwIjc9L4ErJ2@x*tZOUhWhiPU|glJd3394a7Yq14*An#qfqCR|ZL;ZE-q@9KF zV0$uc{R2A zd{})M2bsN!)u)AvZL0nhtV)wn z!@2T~YiVs2$Wk$kQ{F7^u|_wJEw>p*7fM$J;M-@T@DBQ7e%O?`$`I1LZ?M+5d6;iXW|jEfheNKmouilg zH=G~O{31}`NxGG$>zg-$%DDzYc?L8iaP=t&TZ;7^epIWXGCd>@a9dPJA;Jy1izj9L z{|;6+LSo&i7Y@M)IY%i5!WyVlG;BW8h{$|@PRmQ_TBOfcR_&K<__zEa(AC+Q9ws1C zi}Gk9>%`hdFwOMVeLsd2o1KDy>N2;#xSk*I(H6CdM7$vq#}K~i{eKG@IKOr0pswT0 z7Tg5@IC0XXD1$iVPd$o;N?@*WVxPF;rPT^KrCOLO%LP;IzY5d#pbE5$#zWM?WEL)5 z__nXF?yXp=8Dtv}Gm^Xu^-rhYfBwM24@5v&=)G}Fh!`16fa;#1El9tgid8xh&qXTF zhYt-`rG#cco(2~1y(!r}*k#zgH%Wt<*PzPx3FxXXyu+AEioiR>uF5v4j!TJ#?>qPK z+(M`pj^Y?aLh#muVu zLLta?cG$fK5A@yKWWUd4G)Ok%C25*!2Q2r40>(8s$wk#FpCo}>g9Vvh4u7Xy=n zRKqlthkcOBJ*-;zLy&=y_wH$9$&t;-v*RD=Bh5&qVGxVgB0gP#r3(^7 zXKimxkN5;0x-UFo9oPcWxIoIKml3v^TZ@O(ETF#`wfLD-%>$2wXu~KU#TvCUWF)Bb z`r=(EdU^*2#=&N5pgDZPnd7+=WN{`+(TEc>@VA5CzQsa#s)tXD{#!@Ihf7xd>R+^Q z@(a6x?)UHOaD?ex%4=#~+34%Eh>For@ z1AruD&*$!FrX`QOulUNK>L;3`sMK2m66K>b!jWX29-~>WPSKB1D@yR$M5FTAdaAow5UP8Y)3+Vfl~XP1_yCmiwvVxWPFEKr~bj>p6bxm@%v@ z)%u-RS65dCTu276nYFTgX*w%BK*_e?EXhFu^}Y9yDnS5LEva-}DZ5KLN5zVVmsnP> z3fwDqii~vLJ3hU)M&?TN@bGTP8WKQ{=I?rMl(D&F$@fW&8+lJR5Dz;ZUeRGHieT%swF|!}?t7RG?aBIQN0;dwQ#Dg)T`X+Kr< zVBnD#E@1RMjR@Q%#X-$nXu>S-&g!Y^kW>6sQKr6w>;pUT-kxEiI!GPSJuu*Q9w@gLnAgMO5dLwMlXHfp;gV0Cd@6 zjp9h!K_ZdxgPU{#QiNCSD}*!!@xXTDicd$d##gqBGlrztS}=r4er8U^2vR_^tj#62 zN)eOlp@`rQoBq0JR~kujAa%FI!fc16j)-LF1qX| z?%0Wff~jsAD7S{ElcE>On`8p&qGk#fnbDqY<(oa4GViMkG7Ih_IUby+9{HCQ-J!;vWp=9X!j%8w&8u2CW2@`0c3J#7`5Z8|rjPR>%vM6Iz70+=ph)!%7cL&;Si z%>pP>hV=LL zuG#VN@twSQQ6!~%3RDR>)Sf{4Gg!}ua2Gl9Z-o4xUlVyD#%Rn(*wlzk9xe1rT{H59pVrptRWQjUCx&&D$tQ*hr2=>ddW=gh^hp z#nrV3>P&d$7*IhrLl?#{R%;NrWQ3+qPr6%KDYsG(xvw;w?tdClpr=S{y;C0W@xm)e zL^?BWUc1Eu;^`O1xse6Q%b`NIBxsYmscAZ;Tj=DTYb^eHx|dDer&cSmd_l`CaQ z2M-=h-80~iEOI%zaR`HvycG^K3Wrh;c?ic%`Qz6Y4sqC`7eX&4Fju{4>i4kR=|8`H zR3OqhsU3h(HpCm=zau`4CIv%7mFK$Hc+kJ_R|($EIu5C74r-bYdKkpj>UMfVSum;2OliXehk z{^*Pn9UX7zfz*_=<7RLP&Mkg?(4N}^$b6~<9KKaDk-Y--6KPVSufd@D$QsxA0eM>A z^oSY$`4hbb?N-o&kQ~i48R~D6`^~(u|5pI1@{5S}l?|y=o4l5!-E&s$(*P@^ND8%? z(dW*kFZcT}5352l@hXs=h_=!Z5fPN$Xuw{r8=<1G0K_54_)Kh^_Z(D2^Py-4LQF#t zn?x%Vieg3<@V6ms5wd>89OxOQ&6q)kLvV5Gis;FV;=?CTo*X-VJc3(^9Aj;Zn$8x) zT>1i6_^#pQ<>i8GPA#C{8r>WQK?oDls7hv9UZbs;MRG0g`N*)$NktC3gz(?`?xH6v zNccHxQ450Irt!LWba=&DrqCZ0tLFDK6fgFhB&B-6ZFfrO!bKJ`D}ki}9(J0q)kokEI~V<$jSm4dzL(0{V@q8!Ad>BX;auI0#45!mB}D zcw#Zr9*oB^5)!^>TKEbQqMFN1@;eFx?Z<)tF+fEaV-L}!pu1XIyEab63kBu+b1+V1 zMIXX>-JmGY#AI05-oZE;{qGw1Zwts0T@gmG!GfspFFTLH`h9#ABY`H5O-QPkkz|bb z0gBFB01w}GKd6vzUrt(Av~OVa>>VWh8NB`c`sS?5d%EZ7ZP?I+-i3>xC2OHeltG1i zdZj9j?DgGSyrA}1A{?%V!@@B-)#_nw>gJXXq-=(CZKrW6x?&C?VPx+(dGe$g>g3S~ zGT28UTSN`l-@~7VAQCikcb#U^nRG zVEU;AEB7Q4bn1ZG13hCGn8D(A>~YlVV88Fvx;e{ylPO$c?{Xm(Vc6G^@-%`slceJh z0a&Y#;~F48>Y%6r6HGxW0V^c>x48?Gpp<+LD9*iLyZn&FTD9PGBP|k zEkz{h(lau8F2=XSI8l3IPix6-67$0u$UcL_g$AGzgUt1&d&2@7XxpgEh)aFl6_d3s zNQbACubYb?_Bals^t(|!wP5u3C!*RIhu^c zjCNx}q-nKdiCe^JRDiA_638)c*EJg;qh!FCiWoQhz;R5j1et~}67NP3DAYaS?CiV+ zO41ciAM$uKrq2h~mwA?=u5Zx>h52bw7+8*jllKy5xH-k6SJ@Xa;XBdHlWdH?4 zG8916cEFFNOren|zo%jYdNe@!NaGJd@fEVae+^#w@#~iX21f!y^dKdpClvi58;(Q& zsz?Ltx}+IwFZ~xf06|nEAaHe&iq}Akr2r366AWND^?ljz7{{>Za$nYg%%v9_92pzC zfa)T^S-Q5iuda;zLfRm$q;y)WZ_^;O;dp=^)VIX8Kn|e-1wssc^kb-tKC1XL0Akd% zz|DkT17c9J2!ld26I0PeRP%rxv{#h74z-OQfW866x)YpZ%4xvRrU_c(s5=NgaxK>I zljdj{%obuIqaJKNv9#LQt;{Sey0gjCWV1VCYw1euNmv@#W^QFY9HJ-I{W+ZLJ+*SL|4J=v9Lcm4_{cSY5ACYHB zY{!3^CKa)2MBRcJnO_n(5DU0m$F0qd;gD!11#W}vqX@+)YVbipxq0! zekNKB^#Mv@_S`=xcO2Ke4#4nVdz1p-gyxU=lpuRDY$dSE%%~*F6htP$A>F%wzXn_u zx4Bnw^+{?7fN)kCzoHp#xVQx1Qk{PZVlY;>y&lv-)2$t^sek*{&Q^J(zEPjwgA^Ng z&KAR!*3ht~A`wz)Qf5-du?3(BB7~4hHE={UL!^7hPo9iwZniE0uI5;Wn342>8W9nN zrO@j_ZPe7z>dAQXz&GdSqM^rsuzk((GiT!H$Iz?8p?(M-XKhP{k5!o_EG$n61sX{g zPCk4HNDl=q=HxOM`3XQU{K)3^&P#U1etLmY>=Zh$VA1GRbY8KEXpYXEfECp!!d$ZO zKq`H1L8vb+=T|MFo^~*juz{f}$*NGF6$l`R-WWcdz zIERp#EQXfz-!3oFDSV6!IZm0gDE%$*Y@Ubcc@|YOUDDO)g3rY9s!v$UN3X(uoXE*h z=|X}?xOIU)BzH3aFzF}Q5k2a((`7`{!KGHLY|aL|Y6=B;Em~(L0~#1gEM01~qd9j? ztfGyC=ASQ7@vHXR21+ptOmFYpOiU-l?*{O~%Y>2EqoB*2Mp37U01?zEB#Ev;aP!j{ z(>@k=94I2HAETTa2Sn;`ANf-%Y6`{zs3ayOx#BnZ`+oQqp}L4M#h*jEuwtlMl6-kj zb6yf;&!*RT*lWmm7|KY%(~|rD0r*c{{l$_zxmVR(YpLxz1u{2U0>TG&ydh#RRkjga zdTL@~BIViCc*d1`>oRt_8Kx=8n0P%9Z4U}v$W+ij&whaSIIg8IgWXfx_E^*g!ezRe z0rNi#k)=M6A;mrMeS>Sn-NrLZs0T!ZdT}nW0d=ejxrH;E)9iHCiw`R4fcO#I630cP0<)JClkkvO`oJ_yd}n34toLq!Z^N$6FN zg`;$I2klF(ifUlvU!t#$hOR~_M{}-IecHykVCb*f@#ae5?FP#s1NezfoGqX!xx}3y zQXB41Z@qN=&!#OALJ!Ffy(Zq33mACuhc}Z}j#qStsB~T+G7ANFgxZ|L}tIZxX>jI#dj-GZVo=$S5e2;c9q% z0I}22UZjSGL3%p17&;5Ed$2GXmxu&HUSp2*nyw4Ml?Bk#V>{vi)s5Dlnmr!`aH2IY zSFmd&iAb>F@(LSja)+#*#^IU$A6N<5{etf;yQbV14x#$sxi=u|nh?KSVWE(5USkO| z?8l({l7mM_hM-Q@@2ZW#%>jm_!zuu~kTvr8%4jusPiYiX0!5jS!@z-hP}+7lJXv2fnm}zC_$i(7V%1>1vNaAtq!xa`)nZ zkMh$O7@oxrm}c{6h+^?8Ng`18qx&L?8P{0+-Ueu6F-3+aXLUWEs4fJ9;9uK;VLEZ< zXc2nDZDiptV{o%b4KTSB3Rp;`q=*awsG6l>AB~a>T4P49RM?|`i#!c$`JfD94* zHlLM2(<>6=zL9rA`bfrcS<0vIlR8F5;rbgl#v#Gld;HgDc) z(j%dIbgO2}>zGMjKNbVs{q*Wsg*U`6_t!^3Tb?ulfVq0Ov4S&~NAV%#hRFO?DB}U& zW0K9&EP#b?#emYmMOhJ9LixixwuFA1vJ&o78E$ZUdi9`ry78U)leY2ucWf@-XCnq& zxlBo_Fy`;zj3Z9w===VGT#0Fp}dgN;O~yL%1-48_zVnO#vXH;DD;pj;(-V zY&5b?l8b=y@o*3_OaG_mfep8ej{K=O4VY4N4wzjoq-jRO0$rft+aEFHwy^-$_l=E_d$1W@W(dmLEBKbkkdL z<|^dzs}r&vD(uUk1RyTE)x=Q^eIjS+Zk_cA-vsYttxHYQ9}OUmdJMcN0}2^_t+9J> zp3Z|$n)plx>k|vNX8X;aadd#`1a_e}oO?wFjsr-U>9WYm(LylYZatv7X8pDrv8jP4F4BAfOIqE-(xDg5FbrL|)-FoOjex`)~#%TU<> z9(yV_NzfeSi9PTrouRYr|3PmrdSip(3fsMKT?p~sIc482#IM+6itB7boMzJ=5$u8- zg__^tOY+ixi}By=D+HH#0;m%JzYbB2C?X-xSIg5Mr*B|jM!7fvQV4V6(d-&1)ocS@1XmY; zL+)FFOF{V6Lo}&M`?LMC)-#U1F_~!P^*NxLDN*@rj2FdI~T&G#%x^mN48&K*3c*5pSs@%<7H@!=g7() zf!83A3r|lM-n4}VpnT8Y2bAD}B_Wy?g3b=N!s%2(GrYKD1EyX5XKdj-e)aZ4RMfEF z-7$_L5=XXh5`_X>Cxg^48ld^J_D%uW#@rN~^qas=Hl@4skUg1PzeH7dLxgSw`G@<* z6`~cjT?^N7G(v%(#O*BvAP*F6;i**R-g1TD0%G*4_V)D&VP(mwshPM>ry7eVHGlp= z-;Y~x=}#9>cnoAB#0`F*)jkGV+mpx6oUtNy2NlQO@I$PB;KOKKATuc)wuwKFf>Jp3 ze#b&)gKANSbzziyYw=CA)0u(oDXL;lBEy~;_!0f*@L9EJ-?{7jP|rB=(HSiP9TuX( zPZYTb&qtm;J1@KK;gg02Gy4NST;aM`Oi|(`3f;PyXj-sr%PueI8=GmKA+i7Gmk6k} zbWwq@-hCcndif<}8~cBKyiK0H2E2e@)g8@3H0rwojFD||ma-2K=kK@|dt{2)SfcEk zYIxE5_%vzqFeFtV4gnxU7=-L7m5d$$Vx7XpEBE0Hj#9y*HdZN%HFM-HFfQesXAr`=rgp?qEEgs6t(ACD2Wz;m+2B88!w6~wu}`@daH1Qi3Rep+JYJk zr#gg(w-Xd^EaZDGNLR+Y3Wt=T|KP#w#cpSjzq%p4uZ8dC_8%(#eheCmc-?$tApy4- zynKATHK0E~JOe(~Bc?=S*rF`xE8RFi&75>;4etD!1YS@HFm5Xq)*hhseGBj-x#`x3 z!7NnHsaz<9iD4$kodq=HV93d|mKPSgOTck~?Uewxr9^gzVkk8RE%a!VO+s;9$el@T z@9WzRp0K(Za|b;sI0u90a`oeWRJ8U|WCvD~YYLeTk7?A6Ib=dvC6Cu}Z!!q4%ff|4 zXt+0_Ml=v=YE+qc#uzHe%2uCSwZAw5C<(QXI8^@Vwl(Ut0z{B$;MaYCJL5eb9ps|o@O#B9z+bQJ!bHGU~K|o6OYAc7TFohLc zXR2R=+gpe@x#iVnlrf%4wccZ>fsw2gxkU_@@_3g|>KWvjbT>*Zoa$+KeB(qOYb`jz zg^eqn)>p6!?|<`1f*9MMn;17g;kfqu6*H&OmKLkK+dB2i@y6zX=2|= zR|i;l8q5R8VS}OX-(2Npzr2D41jx~3f=zJXE;=p!*U&FBL)x>(MQnm*YTA@Lnue_25Pxl*JNE3Sz!BL~4(d_p2Gf+wn>SU`XiYb%Lr zCf!{_gRg!3IMte$m=Z%rq%ou%p)z-?i5<$6>RZr?L75<3*<}K<92?ehaP+{F2nJsAsutlzrg{~(hLOuGU z(S5rfx7Xwaj~>7bi&}tSiY1GVz-{W|qL+Q}wRof?9$5qCkcyYCkU~aYyqLub@A~uo zcq{fT0 zlrD+PL}k2m)2qisEQVF)I-lp;;ATrzD4jx z$|wVLu6!PF7ANV+s$bcY=dQBr>h4C0%mRB9;0^U8T_s2C8uY&LNNs8%=W<0&l=u|b zrp7XKEl5g8+-=4so)0gt+$3+Aoz9BvA|ym91K>2)eF^F#o}M^dnTT7EYbN6wEIPwf zmxuTw3WUcT+ysT?&;5sOo)J zzM@sU2J%n;s=UegEp*~(beSlIc^U}-DqQTlpmnyAqt?KBQM893(N8D;6mXaO>s!|# z8pDNa%@gpD>liyVays-$BhW>MAC1w8))m?u$;#qX@w z15%z6Uuk=J-;iF+`SUf2<77cH8S5f1Gbgzmf*Uk1k|foH~*s zP_QZTP2tEI<$!*m`%@8r)sUyH!OFMfA>NAsiY}y8M8b`u*|Q%#^Yyr_qiFXhi4@_& z4MHC6OCbHp!dE^Y=+S~Xo}*hWfy7k^7hOWK+Zs}wzQ)7EJbTLlD|RETST^z zy<-?I+yaNK3*EkuW8I{ZIm7_2z*1qiZ!dL4S$YxN=mQjkPE%p%>YBs%9-VgOw9=8J zz>M@xwu!Cjn94J<9$7uQ!lHw)Na&c=2^393Ky{((h9FLwgL)d)DWsK14}v9mbdySb z@^OxaQ6GYAJ(=S|m+>jW!y=fQ3X#wL18+9J2_D_wY7%b=6y^#yL6@T%K<7&pXw^G+ z?mR%`1Ir+13YN0fC*BkkKV1n$z!qj8`<$(0oDPZLh7A^A!D4VKd&1uqi0^Lad0Dpi z_9>C2k8!&Y71c!_S605e?b3cmbEFU>+76xfAOV1c3gQlqs= zd^i$mXi=$JhhPI&!}f!w>NNJQ1_pdjb=a!u65Sk3a;?9(>Hyj@4^ZtN-azH=JuXIR zWJZL@L9&{F(zTRD4V%ORpJ0E&aNOW-AZl$W^6ofPu>FM*S71CIdEhZqeF*K1z7DiG z5J6q#Bai)tCCWluT0yNWRE3sYytuI$3r3C#S5zh9@-|AeSN!~tG=%4yW_Aq8HhQ~L z{Q!ba0y>zO5#8%U;2asrkP)`tZ0O6E#c+kLRCI%p!92B zS{BSPW|X53c)1xcnnHs{8N!7mE+Hhf1?qAT2MVM_6T?cmCoXSWofl@1Xk1AJbUo6S z(4|xuh(55UD+K&%K5w<{Ls9q^nlj}~TR_&urC!%DlONC@q>xM+S8V93+o3g7Sq4`; z<7yuR=s$2o0RYyL2Zu7}ET}wMk=UlIYk29pDS9SX&r{`&M9A>fZ7&tbAfi1Ah4|I& zC>U|^Bv_8;V)xyt{&`@tb*=(x`V&W6uZ?^?yah)40B1M_gew^Y1~482a6zUdkHYxh zYjVtn2d;m~(P;y!Vd5GRPDr zLLGHkW;br$dJ}wQtoYoyDqLt)C5H@JAJr5POP5Ur>XM8rhoI(70D8gfmyy5}PK-*N zosfSf!JiCf0V#+O1&3VexYJ-xb+9i$!k*!dqppxsMtfL;>W}?~>s&H%j~BO|C!1;o zp$177P;9A^wJ+6B!0qNES5bhYTcu8eWz`4$48d_3C`Y0n@QG=r6Yiq6ehw7fI7y{+ zp8mq4qfr4LUlB~A`y}8+n2|fUI&~}EJ_8OOQML!Y6uMv&@S3CJ<|v|1+koivM&u2+ z4R0mdl}OD3lF5Jw{sx9=Spde_1^WqzsLCjB7jlAU#!IT`^QkpspH- zj(7azvrx(4BBLN;rm6Pq?X3zQ7NUj1k{Tu_4I6C`yWbdXN?a+4a^#wFC-bW-&Ibqb z_2X;ekW*MEGGY{MOwr}83IjeBY>!kSNV?xDZ;oDJB)iL z7V_ChlqT_Z58`dU6=jQ6G#c6Jg_T z4~t1J;de^C!3b}?itc6Je6xJ3Cbo_-Ta${bZkrmCXD4_-~4s;@|x^KpL+q}w&P z*9AASO2CP$BahXC=8!D~CVeL~=WV32qudTUaBj^FmJscsfh!H-AAk$BKoQjy`;S2o zXHQxf6!whZ+1SZwyg7}p7;vwKSV#Xq$^@S5ekCO#Z{DufRjZvj6q)NXN1Y&$NT_}CPdWZ$HW`Lzn&aBemoj? z7;h8Qx0jHqqyLB|jsH&u6;KGtXr407Ob`nd{(n@xc|6ta*F8=eky0|0p(shnkWv~H zQc@vPB6HoLGK8X{bP!Q8M5Rz9Lm3iM5t${5keLk0&>%{N@7m|SKhNv;`{Q}t&+F51 z&U?77eeJ#WT5Errgj-i49#t%lbEhZeU&NsY06@2FatB2}H(*BWts0@C^`O+Rp^X8R z3-9%6^!rkf3`N6viwq~Ql8T|Wv_{@W%?etk3xlUUjF1lgWGXZ9@3Hl05m=PIaotD2CL4i=?DIimaHyBOITMOr~}C075NZyS1$D! zAJw?HxJLxlU@PDz7)V+u2PUIfyg-(r-JmpICB_Tl5&Z%X)^f;rCDGJJumBT8oZ-SHnZ)qWDI`o6!7}I0SUW;~?EKy;A7U3}$AP4_kpH-rr*s+kuR!I$MIQeXo zmnZ&pCmuucp^RNy1Cb3oKn;RP zj|AYrwlBt7Co?`;X^1}&vKE&J2_4Wa90IVP3**lOrr67ZonSp2KfB;4LdR@=Jji;1 zz^pQq|0WT{diwPGxTUUmN~Xje2o&6;3)fQ!U6{iJ_D9)*`c7Br`n}Cf{{U)huv|SJ z-Mf8zEm5LzZX!9@`1t(I*N?IBV~8w?7y-vlV&-qM|)8?9X^gGMWz3ZHRc(ie1fiRtL*P_<9b*dbxt`EhHbq4+)^1Mz9s3G}9D)GRFM%Uy(9Ih&e0-K7U!@5p(CvX99=2tA;^%X#d`moS z0mvq0&ApNjXQAnzLj7dWdBSiYLny@JwFc-*vmcsHfH3-T-4x8x&OCZ^=?1*vw^Smc zp8$JA1JEL_5qe1u$Mmc5glAjf{gFEqRCg3gt-rjF3$-OTrwwUX48N+Bhx>wJ^udy| zMq>FMIWXO?Y#bQ9^?q4(u=VJ?wc>~1SGgqZ4RjhY^u-UL8VrSetAoE)!sG{HC}?IqJxAQteSi#6UhxkWHv z7CR!TB5eo+l#fE{x3PsGrnwCsxOSprJ-Tsr$Y=>PhkGIfg&QCwNe2ZNkDZ-DiV|oD ziTor^MZQAU4|*T5UEDa8U=2O=iNY9u4z95X7eXL421B*vOMCf_E*`~II?$kU9g_HT z4z~2v#c+M(Xo#O(d++>c?L;Rm=;ch^eNS1|&BPlXK{w9dzByZmco6iG!*z36%kwhaf<#^R3G^JFp(;u= zGh2t_m>1pdaI@)EB%x#p=UKSW+#c*Wz(Y{~@e(ER;azvl(6xCBc;Q_Y*aQ(FL2Fpa zSa=MS$H}I-;f7W)wXPsztM6ayn@(%gSb~s7MN^Ah`${|_X*yQ_bqtY);+UQQlaoh# za2}mj;3U6+Lo{x*^-R}%q%f;;Ykva)oVbr%G>Pd2!6}V(M?z+Ll*|P|V-PlOIN98H zBknnv9t2l`)Tk7<6y5B=JQwgqe!W?IU^j8aCAgR+3fju#bWIPq{RWbuV?=I7eg`*- zLem^4bx77g^}MYcn5Py~Y(_$1$`5XDg4y+OSRMf6{Y9IR0Q4(N8l zXDK@l{n}p$L0^J89H4opjg37BkP~O(egygNpaMW+_Ep~xHTMx%VX8bTzL(wF;^hhZ z4>`CjYeBB^Ep}Ft1=@LoSXT7PtJjZ{Jc$LH~kURuAq5G?D`= zTB0smTa_k0z)k$%zb5CZWSvdy%KsD=aNqc!GcdX%c!@Mt;m23ib8G5k5c%L~j6sW# zAN9x*Tr~bt98_>*=whf_9_ZytuxADkwVUcroM&HyI<-;7hESK5-iY}eq>&2SrYD?R z7@ zMpFVulqdKg^h5ptgbiLmxV@XQz)G)Ny^6PM5vogo*CvlqDQK6q6HK+xckM@i84Z^H zpiWr=>L`WaEHrn+hRSD!DKwG$kRl+)ayi4akLaRj!@>h%v~wef6G+~l(D@_HEyuYB z({bi%YgZxYl6t;CGO!3LalqPu28o08B5vSU!A|iELaf`0NII9 zP{^N&p6pf1aqwIS>0=tJv(Aau&h^5)qLR2H!DN;#u=%tE^W$oW7#1Ud$0vUNt~Ej?O`mR0zi9usJTpg zmK?%a83vT)huSIJZU{kgypW>;YDY@Hf((Pjc3p)%k*iwFvsB_Jp)CN7zbcirM2sd@ zEXACN2t@ca1U|5E=`VY#qSkJo_{Nr7%c~yqEP>>!!2J*a79v=ow2}T-<%f!~8hgqL zam|wdaoV|MxMhtzY5$!kcHoTefAsLKQ{??ty#9D>_zK0NR=CK3J{;!c-HU~I>3-Ws ze1G)3#p0w0aVc@|LqnU$PH4i=+KN7Q4D6b{VWaftSu{@ccM z5%p=Ib}okDy_Ly?I6yfDDgP1?ktZe{J7yy|5~17Q*LM!W`qNw-O`6bpF-?o(s)_GF z#V$=bLKz_SnI$5KyqRzi>;bV8jzURDxrtqjIxs4yVV;2IcMpYJByo?>bSI7;{zEV~ zQzhEHkb_t1I@T0UH>EqennGL7e9Ovl1IFtFYlF1Typ4dYB}uaa#XPLsf*}{9i3iT> zSPoJ@gM|0+O6mn?2H8|N?v}(!_|GGg9C_!oGM$8deduD_BHltIzZwOd6A1Aw!A~(K z;5vO8Ux})5H&Bg8JxoJG68#B;kT-AM>_#p~3MlN_(sQ5E27aUIBl z!t@M>xgQIrMeHno-q*gJPjC~dRPfi}6EGbNt&NFeiFWb-M=BA*?v)!37j>%gkw6h( zPE6XN-SJDnlR|M%0x-msC8)G}%vd(OHvT=D#=Aj;J(I*$hI_-Mf~l~EZOtkg{j~Do zL2L_@f`QZ;pe)|?084W?xgO(Ju*k?@L8w6*}1HxJWdzSco)BzoVIWp45NNtu0aj-@vK`*2a;ZHZhxDJCM7WErH^vPYj5xkx-)4~EH@g31;9!j-Qs$?uToh{8Zt@nO z31sdgeJgMURy$cus{>$>G* zK@j3p2#86FkWXj-xT(CYY0W0(KNh~uz`T}N(osO3aq1G0LWJQV6DYhq1UC6v4p zE%n0Q9pz$xBGDe-jh4H{-#hsZl^AS9Bh@g}^Y)*cuVrh@r3c)-GrtWk7-X&70(@N` zeVM0b)tu+MunzUWcCwJSkQG!S5eguGJuKABcdVRS(T6Z4L>^1;D!!*$$%sS2h-3O^ zd+C@ok)G(L2E8;zy$&%UjTV#TG2b2%VS5AA4FM%oYfgO5pyeI6r+3#K?g+> zZ4f;4GEjocAhcbx?!~uD5bt0OyA~Mpd-`U6Gpl~7e$wZATNeZQNZp0xOBPc!l7)1~ zXyoO8=bL^1I%yjB0THI5SASo=m7V_Q)6+-D-xfQs^S0=EZ5-ja8V_xn>t<4Q`%P`K`lHmR4gRNsY*0Lo<(*R&3NV0CHvf1 zOo=o=ye%Zhktz;`D9Tn+7$;94z~}W-*6i7A5;zJ5IyX(zg3p&O$XDBLPF?2uy&7l}lN*T&2+ZezGuRcM z0qfobd9BB+eM1_rPfE@Y%jz5u-n#LXhUH7#=6NaS8}pBLmfKm}Y>e3k;TC1wkg#GJ znvn_63&H`33wHDGWv=Wnbe9uQCGJ8#n54`uH3YlP87l8F~MqQq2!;G!jR5W zO$pV-fr=49JG7QN_#xa8cb5E#mhkXo7q2$Rmxg`FBT%!FK&sG*?^p;{2>lND4wgf+ zE)*H_2ld$6S?nFwz6T&}E5;1{fOs5Yf4(I+dO)>4nl)4A7;@J{bb@ITZ;MWQEX75d z>A*ZP3T*JKPCiCp9N_weva1f%?~0i-Qh*%bg^H8I0m~_gJ_r^OKKT1EiGZnl z3MfZfAviyiA|izXCMA^pTNbv77&uR%gjD_E3okV0uPUI&EcoXLa9rD#=ZcHx-YI}- zM;t(hA4nt)cYTn`mIAg2J{~)nb`-8bSD>6M;?VN7o?^tM^7<&GeKKzN!to19o<6MX zNEzhD)BdalTn+N{0cRsTVt2bcb*n(Uqt1M?)BAakJMejO(jlH9pN!PSOxZiZaUTmb z*E|$Nr=TBZHU~uu%$0uip%sH^raI4u*Uav6Ey4ZO!t*A2veiPCT>(IH9cIKtcnIuQ zMx0BQzjI!CAha!}sC{W=EBkHj>?#LM2w8*jB1y01=MVk3^Bw)Cs8N%Io5?1la zn1ITg`iKxSv@sq4`l-y5-TJX0Rq~iAhq8XmHjH!xnYimBM=Etj_rRA2^D{m?5?^Fw zW^bR||2{6@-ruIc+80EiLr*#OBel_?ThD14T9Z8vt=tF zioVkuB4gAFAn)I)8>vsB1g2gPv?L1C1))Y9g-evWj2qUl|H6m$h|@zuqVGfZ1ba{V zAU1o+aa@TZ5<>Th?r_j1+SxHvAIZXhMh4Q`%GAtoEY)%K-o$8OnNNb(+oR{Y|&%J9Jj}nYl*OnOtn2? zg^2+O#56CKdQ%Vo2+vFIzzLxPB>X0c9(7oxiOmZz*6F!#SH3ZBt8P=Fal=!FJCZn} zGyXenhb4tb#ALpN#(1Qw=*3K_L+NTXD}z`p8mv!2f(XI_CQrJb>jZ-`Wxsh=zT(|r z6SEDJV$7FF`4_j(=OOG6Ss8iWEj305G?v78dX(`{>a6K)HVk;N3FFqL=q z)Xm8Hg7~$y09~1+A`&p^upFe3l4L@NNUyaqIDaX^54E5W{b=;8ChCY)n>XJww0*t8 zh=VQb=*P^|N9?}<4e8-tKjS^)Rm1~hT@J1ewU!R(N#?hEZ<^ z*`U=F<82!6?333TKNiHFIbi_$R6h)87teKA25t%Eh#{c*JD*!R`=KkAQ5`5zoQXu7-%E3 z$Zl>--U?_Ue@@6Tl*Y`AE>??YZ4(vuh#kSxy-YN_Db)B$pwQUgCN-afN6#z)Ap|>LfV(Xfd8o*A;YV-Tp z*s-Z1AuDNn&!8h5y)Z>ZLQw|r2wJx9w!*`y?I2a@rZ~p$WPsaU{ z$OxN)baO=FC<@ulffO0Z292r(-I1K5wwx|3CdCc0w@@hCO`k7h26Rdj(Q*Zy(}yev z;4uagGo6O`Hp^S}XVE=MKr%&h>WP$#|1kO3QhTYqrS2ObKdtY|zRu>-_991*aEK6I zkm-QW(a=pdsl7Ehysl?rs?h2qrD~1F?qN}c zgGL*)_LB;V01A{+{oiiVv=-ENMCk^Q`4Y?Ke^x70pu<|h;nv?F;5B&m`jqg{XhV$L zZVA5A>c=hi8SQ1KTXrn&X}r&bjve862<;RAuL>L{z|wb-ff4HQ56^fFG&h|lYV?)4 zT{0SIZDCeQp&kw{>URW9O*UEqDS3nhh+4UiP|e#;3n>5OhuhGq$Mbv!H4r_h0(xq8 zziJCg)`#FjB!T{I75R4n&4qqo&e|yJbWm^CPbs+e$azkkt1#i?{LA&RfU(TLgag<~ zB3R@JP;g0to;A4L>X`86j|E(lz9fwLt7c#tAAJa0cgV_7m|_+<8bVpV&nR=j* za=7n?iJ}CwL!Y1fBVQlE5d`v`_L(V5y+pk|)Jl;=lhNb;!Y%;Xpg!G*{l+IIB~|%j z!2!#OpXSmN_1?fhc0qRT4nX4fg()*mjbz5)I0sagS)(Gcjby;!j<3Xkt$#8CqV3e>0Cg(*a^f!p0xf63eb3;C1rR1l~Nx6ZSyw952&R_+9@;q~N8S)NWWSd&Tur!1W03O9PQ@I2VG~#G->_j|q0WoYr?y)kTP%>kB z3!=TJearK(hnLzc8=R%>Z{~eLw9*GzGjqDVQl3Hp7*fRjgZSQ_-bRWq7cgEQ zZ|CC&o&R&f*zhhK2z5Z%vPXs1g8*2E2YS|_ad;PkDrvLoqXXun^hYHD*p7!M0s5hz z$uVv7JsvX#f9%EBfYecuTZeWbu|nHJfu(z|3<}u|9_RW1gwJA2-s}J`{7AW^6|VMvXa| zCQk7$TUOC{{!gC9^g0xfWrZ`l5Gc*FjHjNM%urz;m~?q0F-!Lgp7lZA}9 z5bL4JoH6INC%v2A4gZA|lU%piZRie+FWEd!Io#apYN0QZfZ&m8wzA5})s>f=M8E%8 z7#Ln>y96NT#))HpG2e3S$ivIOKpaaS<2{CHI(}f3&U%?rgU!7JQef-h#X}hIAnmUA zqd?}WsZoW#v9Tm^_wL-e)8L^VZ)%k3sRMhV7|&_20BkK$WAh7dPC!!RkNhoTjMIe zT`jf(?uwy_jg20=hjUZkPR~fHlcWayXh3J5E{{4$)mCSAQ+(wS54rO4p`a<45i>6= z)9(d1Wm9@8)y>&?F&>uMaQ_R`#_D+&CL8`kmDA$ z8V1Q=Zj>Ve$w=BthAICl)wg%IHeEMsPAZnM|KOREkgywyl?=1F8U{#DzcDy&d_*0i zhOpA;Q5n1hIi#khX7#(x$)FE)^!JzJHO)`K7ox&>iVxXhcKd8UYWzu&$__AQ zM^NWNc>EblJa6AMwM9ORWUQaPFtiFpVUn2iizU#2kU1iQUTdX0|?q*+ejsmL2C`^iIOy@A{#zAa&2NQW3y*S=K3Q3d5OCFU~4(eV9#+e;d@f&2oa4$o8M2vxGYq z7$`8GiPfM(#9lPKa3xjorr9srEDcOdq>%L$O$FMI%7m1yyzr-+@0|Ave}iPB70dwk zz7>$Vw2q0%)w}f$QP7Y^ygfR4B+ZSCxdb;k&3x$>iySXw?7kQd;3Iu*LfD3(HIncb ziO>=sy<6KaV=|;%i{no5O|LpU;t!znyyZwBTYt@cU(z+8$$9hEElo5qo8{VB#kC?- zcmo)i__0Y@`2$jJiS_Gwe)9bTIyw(%A(-@m3)=yk4Zh}0(e_v`HPo|1^PwZi!fi<_ zqrzjxsgKD5f0O2nK1Nm1QKwXEs)%0lXzNoq27@s#sm_0@-rUBfv z`aVy=rctxDUg>e_Q!WaEORe>LEckW_WVW6kGaYTM?UXTj9FUFoHoE=6%OpJi*r!i< zRA;&v9+Kj%L0@uIIMXe~EKhjy$!i)3-QZE!N{%=8$F#?i!SUoqjoEsCR0{1XG6vZ) zXZ?`|hR&|X*Z?&s+(w1#KH&$l&P{_(wdNgM!)Y@i*}BmPuI%A<8S#)kzIR+sZb{CO&R_eQU!Je=y+Y0l^#>XXbePQE!ML9S)zJP+>9mKleqACB? zO8X6K@sxQ41uKP$9+&5G-28JaW6|Qp>Ie%7$W_E?f>(gF z8C2%p(!xTvceCaf$Z&ppNQWK`tVQI`x!<6E)~!RnE#~fKm&B)Rm;L&1u|+Hw<;i@M zNPI!~j=3w)r9pF~oflPncaZr;85tSKfJFu4W3<3idO&`^_!yX+y64VqapTNhFi^na z3B=hDOo=x_g(vAgzi_#rDLQxV+-Q`;PSbw|I$=l?jk?jwOC@InXCWE<<1s>z@Bv*} zhns3_^jwUL_%X@{YbyrHf6Tk+N;+@OoKb=?W++|*2w>HCT;K0h^W!(&zJeHV{TH8Q zc?SL9FDM}6gqB04xZbLmbBI4$6(I6fgcrw>)Bs>$h63SaaG zjm>Pad?D*eW$s+rhFHV{O{j1pwYtae+O9Yf!m@*WTH_^S0 zf4Q~=C|ZWlom62yI3(PftM}i@H(j&~pyK;hZV)tP%$hyha%v~zJdCw@xVc~BW9TC9 z!(NQYTZ(1bJ_YS(!VAp$YQ{+N$NnHip(a2?Ui7WJ$-z^#AVLrE6Tw)>^rgH0 zs}<1-c=P4)=!lj>ic>12>SEK#5IGZ#-ph)ydplg<_&@T*b231A_U^v6>8XuiUtPh^ zSl?lDU*6@iVBWkHhw_|RerG>_E=hK3K{3xeJ1vdVt>?LM7KS+TTJg>Hpf^RQONn+% zVNd?~1+`Ee>@0paOb&Z`*y1ll=Ol8>ovTf$5q=-9SI^kkKX)Y;9f9O8feXH^hW{*NN)hcxy;}iD1Q5P9}GS?-8k!tii#XHWO*8U zo|IX~0>HV22Pd;~rJ8`e^;|bjeI%}(O|JyXHUwP30W*omB7rVrvz*>ow5d@}{moX) z57q(UxxBLSbJgQpDD2_pjx zvQHu^5>>25m0_?5}E7N&0h99@`yBwj3sU}^&{UNy)pQQ<qDs-ImB^^^Kurc{a*@$8}H%TtDt;z{9Zi5cV3?e{#eWMjx`c z7>lXR!mbu9O}Mdc=cG=^hweswt4a$PdzR}reJsw!6=eo-i-syK$_x>@_l zp$s48BwILFX{AqIp4~@tlQ0<~K-tIETKU`YK&|>LI2Vk-VBO*(+lKU)(PAIYRd2?z z(@#DG3GimZB{)Ahbgus5_>S$yDH~A?uEsGSbvi#D5l$0T5`x*T_&?E#2u+=UOArNa z5szAV2EPf!Az@H~V)M`MdD2ySIwg&RZ6gLFbcY!p>A>ea-ugCi)gspxRa6X9erO9% zr*a`yF9P3&<|YD;k4J&g;25$N2r}J`vYW*R?q009pu@0}c zmCxGu2=E-i^Z0<$x!`&!jYq*KS^)1^ktlaF3M+L|(h@b=dq#F^?ADnH;8{J%qyk+vt=e-rmq0fk9LaIx$sse7$>4)Q^bFXd@ z4#hpKt*u6VD$W2o!%zY_mT0S6T8g^Od{ip$c@MxVCbo{j8eaLw_wQI#?1#9{8?UU8 zDwg3ihFa=A#*~FC`$(Q_VZ`OCwjiaZk0asThcv+sI_4?%R>iR&5?*;dL~8QvNS_XBuit|A*!SO2T2aJz z1C+)>s^ZUG_%?8JM{PNK1MfZCxiJ|O6N+*#ie{}(LU4mB zXE`Q+qI>uOi!j@2_m1#f5)X+Y{#2S(C*ueYj_u1+hX`78=iC?3kL0mIgXlCs8rUpn z@A?&G{f%A)vGj9GluHLePVNkfW^Z1l<11CqjeDYsQWKzGwpGIb-dpI68wEnDo0%7< z!=wJpv_Va6Wkv58s1ey>EL%dQ&P!G_w~JS}gvLGDaWeg!+yg0R)xFj^@exJQbJ9n? z1m;+XiXK1mCA9lBYff2uHHZt&8rLsz*4JQvg8GY8sXfNJaj|v8GGlL3{(ML4CdNKB zHO!%FK5O68)uk%`B8=m6Jx-rB7hb9Ro^`7W6^mpz z-D{8P?@zZLP8m4+HPbfAIwm5?X9rKfE-qW4e!#W?J|eo9~RLQUtK8mDvjedpwUN9pN1-5duy!}?d4cn{RCjdY0&+L z_@9ec=t>ou`I`&Mk0eR0D?UC8r%g#njM=Gy0`Wt!FdHe|NgOZ$`Z?G`vvv3TH;d?x5D$UDuEEZ7_XxE>hX|EsEv@RG!UpEal0l z8z>34YS8le^LmfZ*Ri%E#m7cfHd_DLH4e|5p}Fs`TTuG-5=~ETBnfylxd8GbF*Pm6 zxj{-+_BvuwYW2mFl)4cc5z7xFG8{Cj6$Kuf;ke1c?N^{3S1z<;iG1aPlzbV^_A^9i z9Vm!LLuyEH13H+UK`h!Q-a-?yd=PV{s9=gmBQ=t9@7&e+RnB{! zGrC?+Fb+&FHOXi#2d^m=+yMF$j)|dW*$@99cuY(oBHs zqA`jGFJI0KB_#e@;QinaA)`>lV(G>ZYt#xy@jzryaam^?;|*})K6d;3xlPsVj-~Pw zrap%_-B%V$RaI1o9R#N7#&x^wo4MEb(r<|-j*fC}Gh1$Se9FO#CvZ9DhLo9X2g5_8JNXlJ(h=!|c{F>a{1bHsMD{iF*!e^^g%Wr>ijp`jsN zlelUdo=$eD!=QDag{-)MyP=P2w1K%W+qdg6rvm)_KWD2Bc6JufeOvR3(xuBwIYhjS zHl*Jbc)Y4dt}j?B!^od4lXJmr{%f3`f65#rK8qF(gvI#2jGEPZb zSy|Z`Pkp1n)Q`fQhK}-;M+BxsXId37pZNbiw+|RA)~xmKF9Nc{0YwFPYJ`?uI%$C$ zqL*Vq7ihzK+d8{&dEwds^DXktrq?^d)MeyNlq-)^udOu82HwLWoL*@e!pSC}Iuj51 zRhY%R4$;EQkpHes;HaMgGOG3!s%_7+v)eJ{hVRgkFmL~0i_!AxLUS$o5gA&95hV?b z9oO)9b(>?V-o5Mn->Tg^m#^{&%Qn18tU;eYNy3#@EW%jM>)jF*6clI&9Nx8SHU)sx z%MSR?qsNYg;1p=y#8SSNs*6i+5v9Zmx8S64Us)j|RU~99Aj~Oy7Y%I;9uKj&;1JyL z2`dby;*xHhUQUJdgdC**wcSs)+aZ z(&{XE!5Nt|GXDMY-rm(ELjB-MFqdB8!eHFxjgE1PsY|V)p`m_oM90>T5 zp{H`u*k=ygReU=GPs}<(!Nv+-iiwE z8O6)@=9SBD&ju*(j3~445+Ix$D~i~Y=+2ywzx|fojq6b954T9E+*$nI_%R+HHF1;& z>OLY*Z+5KPjfg)E9%Lfsqy#dmW*c>~2p;gc5Y|zoBn^nX{fzv`*FZXh_0TpO+Ok3U zbl*Zm0C}7mHk&w|2<`nhf-57*qkmWf@Lc|!&(b-zzfj6KNC~+5F3U~*@^M=A9UAvL z!H6&E{jKs7?8Y|$c<9gvjj%f?+T-!#$C{{D5VY)3^)_>}lv{(Wre|VOiKnWhowNYC zbQky}a|MR9+Zu3YJ8|})_W6`?YysAAQzpMLyUam|v5?SmiVSb{n%4&=NSgA3QM1sP%h6K#?CSw}8w24x1-CE?b%YvF%D zojCogUmvGp5(+VN$vXLX^~5XMj$=c71xw(;8OM-5Pv=54HH^4fUBuUg)@wD~kMP=? z>j5r2N=WcS?g2{mBfM9mKE34?i`;-!AZ0FGH0Ksxwj@1|Zgr&;dc*w0+dIkk>W_zX zx|B=$1Y@4ljkxVLk$lDrQ9^@!j{P24XIo@yTm2LrWM>WSSv^lCOPx;GG%Bj>m#?xE zOkfq<9lqj{^2+`j-R{6a46rTyKr%!w%#gbGs}7HkWps6yjQ#Wa>4$e|XMnNmpCxpu zhsF=W-yJ0wLu#+DL8;SL&%c8k&$%vGf1K1|6KWjXI?QxuH_$Dw^H39MgFi$W^Uzt} z=RjGbg1=M5hGU=6Vz(aQb{JGy4&{h5` zgj=<&;giU@P_2tYV?1P^7_r@R8y0ZW+!bm}OjRGZuEWercf)S?lJX_n)HfGDf+!jQ zb|!AEXz_t9JC0@un9V)D8rkZ=-jAlgdoCL5@N{2UeYd^pp+97})u79MH{h$jiWR2= zi#`-Jl=1VZzux&U77D01RMseAzXJl+xo}}y&$@p|Y01prd?dlGsu0>?OW%!he`_aj z6nxgP7tit6_RudfWpJcG1ZgJl*-=9~P|JDeUh6}lPt5Z6BhvK*u{wlk{mjC9Xy$&f z`a_!|mJbTV66M~@wR3EnSFuWC^7Uc@2T?^*<)U%@hSpCvC{nQs{rn^#P#J)<0?!(` zodh$V9kISeQF$#2h09tZs@cwbuLf0wgoXD5o^BS)(6$sapK1lW^NP4dXs3$Nc^Ad~W2K0E+hS?7%2Q3T-nJ6F~rfVf1n{Ioe#KUESOa zt&PJU9=KP9vK!*nGNB!cP*Xv`iN{MjPFj0b# zRtop{-J7bw&Q#CRi%ZBwL6L9@Bqva^SVegn^E6L zHuiXbEIobR*~v*_!-jC>+5DjkcanD`pbZmK(?{T=0y9#LKct2Ac!u=k9YN8F9&hKU z`!$(8O*fJ%fL#BUp-WU$)E5-zOe}KW5~jA8pc|@(d1!&wfGqiaS%T{bhBpTQrNp|V z+N=+MgHU<-E~_s&h@OF?0!r-o|GIEnt_jY>cG?W{4s|5>5B_{909xhN60_mNl{_kX=zv#Qb1gH|n$MUA% z3bApLc$tJTepRU5NDirRM7yV)YYTlVJx8JwzdJp1ANtyhU?UlQ)ir}B~8fBy@7jDJB_TYzBOBT0XZj8*=m{sqpq&r ztfTic+O*+x-t>EnGOLU_tdAbS2-+l0c(7za(l5JjW!si&c{J|SpRv6LkERbg=FTA2 z8yUtao!I)_K{1;v&900~&kT0ZjI3+4??P>&dC&8j-SpWE#DUJVPu)5UHr_IPO7$7*72(hgUgH_F7%W=W@KTL=lnUbbUgt+@} z8GH9?&qL3$o-gDk7#xic$#;7LqXp2Eh@n%SKQUwwL(id7BX|1GK>R^~eUBbBGCCK~ zSIERI+h+QM@Tut*AwK@IN`mXd1mK0x zw{26S8^2)p+5qEN@b~76(NA`0(Bwv-Wii;Cvv{vigFV*3`$>n%`Dh4icAHsl*CADAc`B>*I%kD?6IG+w<`4TN=QZD*~qa4$ZCdbdI$PJUI5k z+Lkm}602AH{(X1d<`=1zYoQXqEKt+L_=M7la z730fnS{NekzR%(B#DsmPx41fj$nPH%goAMND00j_XX)AGdl#;Mf{J+!P}uf=`&luw zIC$Hu=IZ%@{e<)W#t$!5we_&?8=C9DfsjA85x&7|7JN|aZeTDLE=WP>o+o-ET}wO! z8W?Yh3Kx}_lPN%1+0}I#JQ|MBu(&Pj)@y_2E#$GTAqlvkIxMxuIrWE_J@UWxci#md zcHuJy=!Pfc+9B`5H=p@^_|>U@KtzQ%vS;Mfp_foJM?>>_{o1t!L&ZBHiTX%RqQ=Hc z-FR{Vx+_BXxa{4W`Pq<1QIi+nIV9m)^Xj|@T7!$x1{qlvN^XzpICUY_jzCuxtmHdr zBVFzFo{jbY`yTb^!F=w%`D0PoDZUGRxPt3{1~s72i3SNbO6`!DDU0e_Y3U_MBEm)y zh{k@Jp2F;T9s$oezkDESsR0^WCd78S6R1|~fqUED3u9-4NPvQVNVO+#SsI7xS)&vK zZVbK{w78XU=iOL(>-%>|J5V1EWVH?+qD|R4bjkgn^CLAH121jg0Vd_98o_yostexV zqc&X|`N%hz`eUq`V$wL?m2iMhAvxZ1+u9A*`(UUctOML?wCX;|=9nZ|1* zCH+Cu4E)0}rUN?faOMSp<7{I>cBUIAn*fD2~wA=mz z1val#_o?aK-!J z;^tx1u#hwoQ|q1~r>a567h~k+d*%q3z#8axh1nE&_jd$Y8v4glPoD;s^_>m$4-8xa z8E6j(EO2g!L60(e!9_6imt*y+V&nz5I*GVkI+mhTR}!bpJKNF$z92^gW5bq4lN54IzGZv9!QfLk_!2=Woa5*EQp z$i6Xr-Mi7w0TiAkz!gKLWcR6!AnbKi$BH#DUhk&$hNm#%iwP929scJsnh?z%OcggC zwnHE+u+8jTwqU*yZ-Dy9VAIW)bAjB(D}nBE>}I5Yjjod50q6!KlQ1c#}q0ne`%2Uv_$aO?WjqdU*7e zqaH#?8fvSNhdUBeQ?xlQQFn9}+dt89fvKm%StD98ITVpd>vz%F$lx z-W%oiEu62c0w)qQBpGA3#J(b(*>&J+GLely&+`~oN~}DZ0jo3hdzwdpVfs8**r!H2 zv(GMy4>T%!+%qG1NGmA~U;G+<1xYRO#edZutU(jiLKm8I(Yz$EI?4LuWr^USqZx-V zO;&I+xe(m*%^uTA={)M(;BBaZGYTk^Ne6u=cnCa09{gC}aprGm*`yh9{)#-F>1uI% z-2?_o7hG5&Xo?;eACfZum5cPTcF&+g)ZkH3qX_B#NZNiPf1+ibI(A)buMQ74R?gk9 zWVGT%acVXGt*H40=ls~4t3b1p5J<$~#GiJvu7Ppl)oGzuMvwBEI`|cjq^dDYBc|n4 z8V!ILccPJEIlSiOP(k;UP``5BW7ESlu{vdXX-i|aqgdw2mRrEDS7m3Y6h&=v-bH06 zjIXPNiaKce3*j~w#`H>vgB=Ho40v-xAWTPNdFx_CNY9fX*ZfK7#(X4>EzI>=eBP}|S9`#uX89*h2deLThMGy(-+zkqttlVpvNYaRU zFe?21uT1hMHWD|dneo#=?tXp%@PSwpW&5$OcpdBUh^2tPJbg% zKE@BN);BWxC*j_z7a3?Syg@=5bRq0%Du)%{$AhTn_bDkWXN(1L=BC;%@Gr8I`+U>C z*%;4$F`Y%SH4fNWZ?BtebF~9=oOk=#6^F>q48*C7FwTYT|-w1hM&JA6VTCP*G7XF1LHV5rm39RY8qh*q51H-A*5d+uwPgAv`ZzIVhm|&^thY>O27(C7pb{-gZU%Ik8w<2 z0X-~r%+EeIwSPe^HQO)5WLK1x?Ez!5>*SSP#D=CvHGV>RV?T5|8V59uQI*9a6&u)E zY>Hl{CA_cuoCEc+{$tuY&)b1D60>f;E|IV8Qj!glkUPdYI67Jk&mT%Jkx3qB3l9kq z1J=~IE}XZ2o#&qbrSs$KXV0?|!XQ3cKQH;7YNG@>vn>2GGT0R_97fvQAu&`3I5!@Z z1`O?J>Ag444JkbLsj~r9A1E-!>fA5zV|Ch){^%zt3G$)KzA<^}cpSK)>hQk2aO(YQ zeGoVy4(0qSoj#U`BKZEC`Pz`O9356np&?2|z(%UaxE@3GcLjss`KL}i*_en#z7sE~ zPt|*gYsWNx>Mp#xC{pl1L}xk7niBfb)gpJR->&J@zfpAP^sLMB6VzQSR z2=4AkjE-h;n>Cz3B1xcE?VH-Y07e8O2{ZSs%+#aI&;4CA!}RxPfy_YmC%88L)B0l5 zU#rJ)=>TuC=E!5di(ZmJ;AH3VM+jB$FzAG4h+o__0aXR%N<8Wop2?YdNRmXQrRQ%w z7X2yRhU4xWw@_&8_!VtKOUue$JHO^~fwr;+C*NqvA-lTxeap`I)aK(?hYDou+n%5K z9?QSMp1*a)X^ln?zGq*(pCgu!awHTbn&hPWUAe*yvmbtONj=ETP-gOCy6(WW6kaGfrhY~-{Of4lG)iM1f4laK2B?$!uJIO8&dBFV965S-S2p$RF zk%U7A32xe0v_TRKBSd9nxbn)5yawHf2Rx)$yu5*HaRzHQY~X##5M7bVu{|M`qsai# z+7cNG~{K@E(-DHZ4+{g8*Gn`ucKBE2qxMpg|>BcDA2l7L=mS{fAPA(Lqz zoPcN^0H756kkdpXNRS&%#DSLy-L^Hc45MfE0$d+$h{!7Vpn}x#ySB%ijiY?BmG=-c}kI>NUA;r_SwqewA`2UvtU z4`_uQtfivi#1{lt;r(z<`~2n0qo&>_cyAS?tX?w&pK+*Y;#gN;SJ4~7Hc z$-k_Eb8=G$tv@k7ynp`&jpu)$$9M`KpywARH{-LNpBO?HqQynWVb}2nheFr4X;T3l zTUh3^AX*9}Yfsw`@$>Z`n;gnerLiN#zeQ~R2ch&dqIPHB4guoqf&!J;SR(v1243-W zP=NQ<-r2d6#>$|)&_@082ruDtXJ>3 zeGp9n3Di_Xr-FYoJZBa=xwyETaV8WPzQHwsjQU_}i34cEX7>>ZA0Hu30r|@`nxQdo zi;6P3C@^^1vIC`Av(AIAp`ra)Rj~kH;48Ta0GtknH)5x5n#ef|Fc>rNcH@&zX+Xs8 z-TADa_QR^{OJ@BM^h})!W$ZukgCisN2nYzAhFO3nXl8Im z0r(h;mevQX1vy#4cBB{&!T$;5nyHZiP70` zEl10zw4~(W&UZ~%uvl0-ur+cvxrG2_LgGb`JbtEU>i)6<@@~fvxYq*=_SZ54N{FVX zqKc59(r1W`>1r{jy_M0(RjZ~3Ro^1K+ZbezX*OQl!NR_d+rSdS!di?(o0~g0R$^lt z21G~g+c_7iu{aFkWOkeF3o(r=9zr`^EU&cc9Xa-m>FN}psD8fBf2+}+8k9eN{aOVj z3X)E7V4tGnZ4;TsDTK|7isDRo0G*tH6M#|v6A46Z3aeRJt zflQ@4*8OHo()IK6i$-hpqacg>L3n_F0)`Kcr)8u!`yqI_^fhIzdsV^`eFqF7O-!I8 z7Gb4XssI?g{x#(?_MNYI74;ZEvx=&+nFY;24q)yOF8jPZ{EOvxjtH;6TQEfH3* z+ZXvB*Pc%V;1BO*DrxhPg-s|@dH}oCv($C|LG@WnDhT}j*fq}y)w7uYjuu|uPVC*| z_&*+ge!o8)iQvR(yBaq!6dmb1AXp8BKk!xM*N;q}7LWg(A{?0|o> z{w+vu*j{RArWQ`j$_3D^z2R09u&Y~%*S3xCUdYYPc&-6^ACF6aH~HVlz+Rxa*bN?n zjQ5sQ^I;}brCF#dbZhJ`)9*WFDyQPC-4^Zig9tSMA-Py+mdZhr_Fxpb_!|G%D8tVx zlGrOm^?V9zp&<#DTQL&sZzJTqQOjIyje`ur*44}F4mADo$uM39NdHb~me*UvF~^A6DA!p> zkOq(9Ha_?FC&Db{a%^mD_I(zmW>k@$1^MG1&$zz(YEcXF$4PdE{s3w zxo~&^W6{oPv^dlepBwLqNrZ02zS}o}epf`Egef9IXk}c9X~b3)bz9J13(Cr_OtNak z)JkSFN)eLKGJj!cTH=ETYNT12{BvpNqeBq;TLN~bKSvTCY9H5YYLv14_AgnCdEy3l zT^@629B_?S&=`0NjS9}@xNBLNUL7>|j?B{@)aPwyIY~6-6r;3D@aXXR!|@zsKI@n< zQ==?8E^wgwQ7sDvw;PWaJI0cWOPjsAgY)811?!ArP*7JBPJ~d8WhO~}JAjgDXxmaS zTX?|{P{!6b6-l__N01aQKpk78GiZjk3_cnMXAo_ql`hIE`3*EX#6)wzl>Z&?3Hz{4j7bwju^yGrCs*y}%qZ&1sEg|g3Z+Df7T zq@xTw$i`AWvJa{UYR3r1?^w#@!hB-5cp?Z;O-s%*{#UN7$Y`t-n$!LYy&ZK8jf3c* z(h)w!HZvN;-Ex3`2>@krhr$T|z9b=|cN#0f-m`A^tX$bX#?{=^p|&6A=`0C=HB2>N zY^>(y;`;iVJ@4)vK!^^vJj2p_951#LhM;lG-WN+2NtOPAYvV}Y{cGs=j{u!BD0|Ma z*9}*<*>mbeUrX-B!Nv?8)r%w;82axVemN#Qh$$`QtTP-QLR6)T zj;zSPi`x6=$jAe5RNvp(8fg?2n1SI@Na#es2=Ot%NTCff!%MI#BTz@JRSXWGRieod-v$LDO`;`g zD>@ojqM>5UA^~00ih?MJQNdMp=?K!_p`ZE%Siqfo@44rmv-kc#HqX(qcW`ha^It@5 zer58el2@jjX9onc8!X^reSdGGqEr=aKq=xhDp`C^)+cJHd)AP z32e+3biv`~;z~^hkhU6qAZXgeD_^H3+3f6xl+3wBlvZQ$RCF|XZ2u5OuE4+0 zdet;EH0WR2U$?g^!?(OLZ;8uDOYm=NEm7niYRJk97x>vT8O<%>SS+0;hqA~|b}!z`O1y1fZt z4XTA}m_}-?1`=td)+a{q*?yfimgost-~kj*N4k}++E8Zi@;NunuBI|uGM+tez@9!V zu;cgb_7m88inhh%!~V^#<0X;A%oVX3J@{C9i%4}hM-2}}+knX97@}Q`(nKJ%W#+Wb zVsUlkkhJDnoO))piW1n~NVmIlai{Z7J8k(aF#V|4nqo=Uk@@kvUkJJl=%TR8xm6*H z5KRE7aZAEplBd$?XjSSwC<>?^EAIVX1d)J-?X3EKkVqokaxGClXYYg)=KY*VWQ==1uIq62D|%cSFSs+ z>Uq(;(H13E9brE_#(wh9UnTqBgO-V1!m(M_^us3i4BDcqIb;0%`FAR0TM5a8DvNS= zH18CH=mv_gqA6flU;5JJNeOXsvgqY^j9gG81txEO8;vZeLslbx(-%wO<%g!X3J(lH zY9X7xOTOJxG0ugUFosf9EzFAUyiZ?+Y}Hj* zX?Qe}*zi{IxgsHO7aQaYk+=GJ~1 z+q;J!$xYt%8v%cDHfuw1W;#>pu35qdI#}zM8>{~DBXzv7vr~f z9aTZw!CzjwPGLS4e~;T0vOC=a1Hw_N8snDjgc%<{cdjbzG(T~&)~_{IC0`^^JTGnp zMY)K1ABG!!D9#}Uj-_B<1+wHaUUGuYvPfuQSt92M$@$9eC=7J+z$G7Jnxe(VhYES_DfXHF@$(|cZh`~JD7C9=OOM#xBRk%H9Vrx(9)VShzt zIG6hv!`^o5;I|);qj`Btj>k5Pu2=opRwP~no_t}8Y9D;H_MiXOxPV?ulb*kLf$+tP zkOG~sLMC4-uTPnkfxJo$6~~4xnIEunRUAn)Rdc5vt7wVQDij3*-J4RZX9*inoG zVb}?pIL-(N=&SH0uUq;fb^{tA>WYnt*?wp`vtJX#2^i|$9?b*2C?T<%d-(V)gdkIk zbN0izr|B!PfT>?2&Vdc-eClJ|fpI6mjTpfsC}Kxak2`*TGMOgMK4M6CHs)xxWI^=@ z^Rdq?3#rE0b8m zUn@B5b5HAocGn?)>Od88@5b3k@oqORdR3ZON?D@b`44ipj!6Y%h~KOqS^K-j_3v`0 zTaKnT{-B5phPfhKw*U~PQ8|8{-qsVMWs=SSlmsDd;Je{2B1&p{1uZ|hkZI9VriN80 z0g{>DmQ`};_~D|4(Lk_TUgh=Mq-VYv|Py;e*YZv9of%*_u1ua zKHAx{?H_bmcM6s$!}95^%1H9QrPogQiG&<+1y4`Qq~ItQJ_W z1K9s1u5+|9+AuEXU@!X^>I6dV2S)iMN$q4+=1z3$h{z%TNKBld;Ep(m=G{BMd*_Q3 zSG;DcO~P~n;s*_M4z#N-1f6OCHW?ra_L|A+D2(RxLHVM5ukdS_LiE z56Eay9hVc68VjG6a^YZP%0Jm@P7;f}(XH|SEBxyn7E25#gpwQsG-Nq&V1ej%xqJ7T z<5UEey}Su1G%Y>-=3YYf5JAFWvD31t(MvN4_>A99EU}krOu*Uy?#m~_70emjdHkkM z?d=<0)a!Qg@ZtW;r+)75f1e8S?ZLqfg%58`U)MWR@**KMdU)5l22ceR;_IZ!C+zNjRTP_2 zlw~N&j>s&?=9?6xRE7B~L;Y1jI%RM~a7ctIY^E|iLaE$(F6p2Dcfj)eth}5x;($

+#include + +#include + +#include "platform/common.h" + +#include "utility.h" +#include "queue.h" +#include "audio.h" + +namespace audio { +using namespace std::literals; +using opus_t = util::safe_ptr; + +struct opus_stream_config_t { + std::int32_t sampleRate; + int channelCount; + int streams; + int coupledStreams; + const std::uint8_t *mapping; +}; + +constexpr std::uint8_t map_stereo[] { 0, 1 }; +constexpr std::uint8_t map_surround51[] {0, 4, 1, 5, 2, 3}; +constexpr std::uint8_t map_high_surround51[] {0, 1, 2, 3, 4, 5}; +constexpr auto SAMPLE_RATE = 48000; +static opus_stream_config_t stereo = { + SAMPLE_RATE, + 2, + 1, + 1, + map_stereo +}; + +static opus_stream_config_t Surround51 = { + SAMPLE_RATE, + 6, + 4, + 2, + map_surround51 +}; + +static opus_stream_config_t HighSurround51 = { + SAMPLE_RATE, + 6, + 6, + 0, + map_high_surround51 +}; + +void encodeThread(std::shared_ptr> packets, std::shared_ptr> samples, config_t config) { + //FIXME: Pick correct opus_stream_config_t based on config.channels + auto stream = &stereo; + opus_t opus { opus_multistream_encoder_create( + stream->sampleRate, + stream->channelCount, + stream->streams, + stream->coupledStreams, + stream->mapping, + OPUS_APPLICATION_AUDIO, + nullptr) + }; + + auto frame_size = config.packetDuration * stream->sampleRate / 1000; + while(auto sample = samples->pop()) { + packet_t packet { 16*1024 }; // 16KB + + int bytes = opus_multistream_encode(opus.get(), platf::audio_data(sample), frame_size, std::begin(packet), packet.size()); + if(bytes < 0) { + std::cout << "Error: "sv << opus_strerror(bytes) << std::endl; + exit(7); + } + + packet.fake_resize(bytes); + packets->push(std::move(packet)); + } +} + +void capture(std::shared_ptr> packets, config_t config) { + auto samples = std::make_shared>(); + + auto mic = platf::microphone(); + if(!mic) { + std::cout << "Error creating audio input"sv << std::endl; + } + + //FIXME: Pick correct opus_stream_config_t based on config.channels + auto stream = &stereo; + + auto frame_size = config.packetDuration * stream->sampleRate / 1000; + int bytes_per_frame = frame_size * sizeof(std::int16_t) * stream->channelCount; + + std::thread thread { encodeThread, packets, samples, config }; + while(packets->running()) { + auto sample = platf::audio(mic, bytes_per_frame); + + samples->push(std::move(sample)); + } + + samples->stop(); + thread.join(); +} +} diff --git a/audio.h b/audio.h new file mode 100644 index 00000000..a1c348f9 --- /dev/null +++ b/audio.h @@ -0,0 +1,17 @@ +#ifndef SUNSHINE_AUDIO_H +#define SUNSHINE_AUDIO_H + +#include "utility.h" +#include "queue.h" +namespace audio { +struct config_t { + int packetDuration; + int channels; + int mask; +}; + +using packet_t = util::buffer_t; +void capture(std::shared_ptr> packets, config_t config); +} + +#endif diff --git a/config.cpp b/config.cpp new file mode 100644 index 00000000..a9cc82b7 --- /dev/null +++ b/config.cpp @@ -0,0 +1,27 @@ +#include "config.h" + +#define CA_DIR SUNSHINE_ASSETS_DIR "/demoCA" +#define PRIVATE_KEY_FILE CA_DIR "/cakey.pem" +#define CERTIFICATE_FILE CA_DIR "/cacert.pem" + + +namespace config { +using namespace std::literals; +video_t video { + 16, // max_b_frames + 24, // gop_size + 35, // crf +}; + +stream_t stream { + 2s // ping_timeout +}; + +nvhttp_t nvhttp { + PRIVATE_KEY_FILE, + CERTIFICATE_FILE, + + "03904e64-51da-4fb3-9afd-a9f7ff70fea4", // unique_id + "devices.xml" // file_devices +}; +} diff --git a/config.h b/config.h new file mode 100644 index 00000000..e54076aa --- /dev/null +++ b/config.h @@ -0,0 +1,34 @@ +#ifndef SUNSHINE_CONFIG_H +#define SUNSHINE_CONFIG_H + +#include +#include + +namespace config { +struct video_t { + // ffmpeg params + int max_b_frames; + int gop_size; + int crf; // higher == more compression and less quality +}; + +struct stream_t { + std::chrono::milliseconds ping_timeout; +}; + +struct nvhttp_t { + std::string pkey; // must be 2048 bits + std::string cert; // must be signed with a key of 2048 bits + + std::string unique_id; //UUID + std::string file_devices; + + std::string external_ip; +}; + +extern video_t video; +extern stream_t stream; +extern nvhttp_t nvhttp; +} + +#endif diff --git a/crypto.cpp b/crypto.cpp new file mode 100644 index 00000000..82d54359 --- /dev/null +++ b/crypto.cpp @@ -0,0 +1,231 @@ +// +// Created by loki on 5/31/19. +// + +#include +#include "crypto.h" +namespace crypto { +cipher_t::cipher_t(const crypto::aes_t &key) : ctx { EVP_CIPHER_CTX_new() }, key { key }, padding { true } {} +int cipher_t::decrypt(const std::string_view &cipher, std::vector &plaintext) { + int len; + + auto fg = util::fail_guard([this]() { + EVP_CIPHER_CTX_reset(ctx.get()); + }); + + // Gen 7 servers use 128-bit AES ECB + if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) { + return -1; + } + + EVP_CIPHER_CTX_set_padding(ctx.get(), padding); + + plaintext.resize((cipher.size() + 15) / 16 * 16); + auto size = (int)plaintext.size(); + // Encrypt into the caller's buffer, leaving room for the auth tag to be prepended + if (EVP_DecryptUpdate(ctx.get(), plaintext.data(), &size, (const std::uint8_t*)cipher.data(), cipher.size()) != 1) { + return -1; + } + + if (EVP_DecryptFinal_ex(ctx.get(), plaintext.data(), &len) != 1) { + return -1; + } + + plaintext.resize(len + size); + return 0; +} + +int cipher_t::decrypt_gcm(aes_t &iv, const std::string_view &tagged_cipher, + std::vector &plaintext) { + auto cipher = tagged_cipher.substr(16); + auto tag = tagged_cipher.substr(0, 16); + + auto fg = util::fail_guard([this]() { + EVP_CIPHER_CTX_reset(ctx.get()); + }); + + if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_gcm(), nullptr, nullptr, nullptr) != 1) { + return -1; + } + + if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr) != 1) { + return -1; + } + + if (EVP_DecryptInit_ex(ctx.get(), nullptr, nullptr, key.data(), iv.data()) != 1) { + return -1; + } + + EVP_CIPHER_CTX_set_padding(ctx.get(), padding); + plaintext.resize((cipher.size() + 15) / 16 * 16); + + int size; + if (EVP_DecryptUpdate(ctx.get(), plaintext.data(), &size, (const std::uint8_t*)cipher.data(), cipher.size()) != 1) { + return -1; + } + + if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_TAG, tag.size(), const_cast(tag.data())) != 1) { + return -1; + } + + int len = size; + if (EVP_DecryptFinal_ex(ctx.get(), plaintext.data() + size, &len) != 1) { + return -1; + } + + plaintext.resize(size + len); + return 0; +} + +int cipher_t::encrypt(const std::string_view &plaintext, std::vector &cipher) { + int len; + + auto fg = util::fail_guard([this]() { + EVP_CIPHER_CTX_reset(ctx.get()); + }); + + // Gen 7 servers use 128-bit AES ECB + if (EVP_EncryptInit_ex(ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) { + return -1; + } + + EVP_CIPHER_CTX_set_padding(ctx.get(), padding); + + cipher.resize((plaintext.size() + 15) / 16 * 16); + auto size = (int)cipher.size(); + // Encrypt into the caller's buffer + if (EVP_EncryptUpdate(ctx.get(), cipher.data(), &size, (const std::uint8_t*)plaintext.data(), plaintext.size()) != 1) { + return -1; + } + + if (EVP_EncryptFinal_ex(ctx.get(), cipher.data() + size, &len) != 1) { + return -1; + } + + cipher.resize(len + size); + return 0; +} + +aes_t gen_aes_key(const std::array &salt, const std::string_view &pin) { + aes_t key; + + std::string salt_pin; + salt_pin.reserve(salt.size() + pin.size()); + + salt_pin.insert(std::end(salt_pin), std::begin(salt), std::end(salt)); + salt_pin.insert(std::end(salt_pin), std::begin(pin), std::end(pin)); + + auto hsh = hash(salt_pin); + + std::copy(std::begin(hsh), std::begin(hsh) + key.size(), std::begin(key)); + + return key; +} + +sha256_t hash(const std::string_view &plaintext) { + sha256_t hsh; + + SHA256_CTX sha256; + SHA256_Init(&sha256); + SHA256_Update(&sha256, plaintext.data(), plaintext.size()); + SHA256_Final(hsh.data(), &sha256); + + return hsh; +} + +x509_t x509(const std::string_view &x) { + bio_t io { BIO_new(BIO_s_mem()) }; + + BIO_write(io.get(), x.data(), x.size()); + + X509 *p = nullptr; + PEM_read_bio_X509(io.get(), &p, nullptr, nullptr); + + return x509_t { p }; +} + +pkey_t pkey(const std::string_view &k) { + bio_t io { BIO_new(BIO_s_mem()) }; + + BIO_write(io.get(), k.data(), k.size()); + + EVP_PKEY *p = nullptr; + PEM_read_bio_PrivateKey(io.get(), &p, nullptr, nullptr); + + return pkey_t { p }; +} + + +std::string_view signature(const x509_t &x) { + // X509_ALGOR *_ = nullptr; + + const ASN1_BIT_STRING *asn1 = nullptr; + X509_get0_signature(&asn1, nullptr, x.get()); + + return { (const char*)asn1->data, (std::size_t)asn1->length }; +} + +std::string rand(std::size_t bytes) { + std::string r; + r.resize(bytes); + + RAND_bytes((uint8_t*)r.data(), r.size()); + + return r; +} + +std::vector sign(const pkey_t &pkey, const std::string_view &data, const EVP_MD *md) { + md_ctx_t ctx { EVP_MD_CTX_create() }; + + if(EVP_DigestSignInit(ctx.get(), nullptr, md, nullptr, pkey.get()) != 1) { + return {}; + } + + if(EVP_DigestSignUpdate(ctx.get(), data.data(), data.size()) != 1) { + return {}; + } + + std::size_t slen = digest_size; + + std::vector digest; + digest.resize(slen); + + if(EVP_DigestSignFinal(ctx.get(), digest.data(), &slen) != 1) { + return {}; + } + + return digest; +} + +std::vector sign256(const pkey_t &pkey, const std::string_view &data) { + return sign(pkey, data, EVP_sha256()); +} + +bool verify(const x509_t &x509, const std::string_view &data, const std::string_view &signature, const EVP_MD *md) { + auto pkey = X509_get_pubkey(x509.get()); + + md_ctx_t ctx { EVP_MD_CTX_create() }; + + if(EVP_DigestVerifyInit(ctx.get(), nullptr, md, nullptr, pkey) != 1) { + return false; + } + + if(EVP_DigestVerifyUpdate(ctx.get(), data.data(), data.size()) != 1) { + return false; + } + + if(EVP_DigestVerifyFinal(ctx.get(), (const uint8_t*)signature.data(), signature.size()) != 1) { + return false; + } + + return true; +} + +bool verify256(const x509_t &x509, const std::string_view &data, const std::string_view &signature) { + return verify(x509, data, signature, EVP_sha256()); +} + +void md_ctx_destroy(EVP_MD_CTX *ctx) { + EVP_MD_CTX_destroy(ctx); +} +} \ No newline at end of file diff --git a/crypto.h b/crypto.h new file mode 100644 index 00000000..82f4b472 --- /dev/null +++ b/crypto.h @@ -0,0 +1,64 @@ +// +// Created by loki on 6/1/19. +// + +#ifndef SUNSHINE_CRYPTO_H +#define SUNSHINE_CRYPTO_H + +#include +#include +#include +#include +#include +#include + +#include "utility.h" + +namespace crypto { +constexpr std::size_t digest_size = 256; + +void md_ctx_destroy(EVP_MD_CTX *); + +using sha256_t = std::array; + +using aes_t = std::array; +using x509_t = util::safe_ptr; +using cipher_ctx_t = util::safe_ptr; +using md_ctx_t = util::safe_ptr; +using bio_t = util::safe_ptr; +using pkey_t = util::safe_ptr; + +sha256_t hash(const std::string_view &plaintext); +aes_t gen_aes_key(const std::array &salt, const std::string_view &pin); + +x509_t x509(const std::string_view &x); +pkey_t pkey(const std::string_view &k); + +std::vector sign256(const pkey_t &pkey, const std::string_view &data); +bool verify256(const x509_t &x509, const std::string_view &data, const std::string_view &signature); + + +std::string_view signature(const x509_t &x); + +std::string rand(std::size_t bytes); + +class cipher_t { +public: + cipher_t(const aes_t &key); + cipher_t(cipher_t&&) noexcept = default; + cipher_t &operator=(cipher_t&&) noexcept = default; + + int encrypt(const std::string_view &plaintext, std::vector &cipher); + + int decrypt_gcm(aes_t &iv, const std::string_view &cipher, std::vector &plaintext); + int decrypt(const std::string_view &cipher, std::vector &plaintext); +private: + cipher_ctx_t ctx; + aes_t key; + +public: + bool padding; +}; +} + +#endif //SUNSHINE_CRYPTO_H diff --git a/input.cpp b/input.cpp new file mode 100644 index 00000000..6f2ff7d6 --- /dev/null +++ b/input.cpp @@ -0,0 +1,142 @@ +// +// Created by loki on 6/20/19. +// + +extern "C" { +#include +} + +#include + +#include "input.h" +#include "utility.h" + +namespace input { +using namespace std::literals; + +void print(PNV_MOUSE_MOVE_PACKET packet) { + std::cout << "--begin mouse move packet--"sv << std::endl; + + std::cout << "deltaX ["sv << util::endian::big(packet->deltaX) << ']' << std::endl; + std::cout << "deltaY ["sv << util::endian::big(packet->deltaY) << ']' << std::endl; + + std::cout << "--end mouse move packet--"sv << std::endl; +} + +void print(PNV_MOUSE_BUTTON_PACKET packet) { + std::cout << "--begin mouse button packet--"sv << std::endl; + + std::cout << "action ["sv << util::hex(packet->action).to_string_view() << ']' << std::endl; + std::cout << "button ["sv << util::hex(packet->button).to_string_view() << ']' << std::endl; + + std::cout << "--end mouse button packet--"sv << std::endl; +} + +void print(PNV_SCROLL_PACKET packet) { + std::cout << "--begin mouse scroll packet--"sv << std::endl; + + std::cout << "scrollAmt1 ["sv << util::endian::big(packet->scrollAmt1) << ']' << std::endl; + + std::cout << "--end mouse scroll packet--"sv << std::endl; +} + +void print(PNV_KEYBOARD_PACKET packet) { + std::cout << "--begin keyboard packet--"sv << std::endl; + + std::cout << "keyAction ["sv << util::hex(packet->keyAction).to_string_view() << ']' << std::endl; + std::cout << "keyCode ["sv << util::hex(packet->keyCode).to_string_view() << ']' << std::endl; + std::cout << "modifiers ["sv << util::hex(packet->modifiers).to_string_view() << ']' << std::endl; + + std::cout << "--end keyboard packet--"sv << std::endl; +} + +void print(PNV_MULTI_CONTROLLER_PACKET packet) { + std::cout << "--begin controller packet--"sv << std::endl; + + std::cout << "controllerNumber ["sv << packet->controllerNumber << ']' << std::endl; + std::cout << "activeGamepadMask ["sv << util::hex(packet->activeGamepadMask).to_string_view() << ']' << std::endl; + std::cout << "buttonFlags ["sv << util::hex(packet->buttonFlags).to_string_view() << ']' << std::endl; + std::cout << "leftTrigger ["sv << util::hex(packet->leftTrigger).to_string_view() << ']' << std::endl; + std::cout << "rightTrigger ["sv << util::hex(packet->rightTrigger).to_string_view() << ']' << std::endl; + std::cout << "leftStickX ["sv << packet->leftStickX << ']' << std::endl; + std::cout << "leftStickY ["sv << packet->leftStickY << ']' << std::endl; + std::cout << "rightStickX ["sv << packet->rightStickX << ']' << std::endl; + std::cout << "rightStickY ["sv << packet->rightStickY << ']' << std::endl; + + std::cout << "--end controller packet--"sv << std::endl; +} + +constexpr int PACKET_TYPE_SCROLL_OR_KEYBOARD = PACKET_TYPE_SCROLL; +void print(void *input) { + int input_type = util::endian::big(*(int*)input); + + switch(input_type) { + case PACKET_TYPE_MOUSE_MOVE: + print((PNV_MOUSE_MOVE_PACKET)input); + break; + case PACKET_TYPE_MOUSE_BUTTON: + print((PNV_MOUSE_BUTTON_PACKET)input); + break; + case PACKET_TYPE_SCROLL_OR_KEYBOARD: + { + char *tmp_input = (char*)input + 4; + if(tmp_input[0] == 0x0A) { + print((PNV_SCROLL_PACKET)input); + } + else { + print((PNV_KEYBOARD_PACKET)input); + } + + break; + } + case PACKET_TYPE_MULTI_CONTROLLER: + print((PNV_MULTI_CONTROLLER_PACKET)input); + break; + } +} + +void passthrough(platf::display_t::element_type *display, PNV_MOUSE_MOVE_PACKET packet) { + platf::move_mouse(display, util::endian::big(packet->deltaX), util::endian::big(packet->deltaY)); +} + +void passthrough(platf::display_t::element_type *display, PNV_MOUSE_BUTTON_PACKET packet) { + auto constexpr BUTTON_RELEASED = 0x09; + + platf::button_mouse(display, util::endian::big(packet->button), packet->action == BUTTON_RELEASED); +} + +void passthrough(platf::display_t::element_type *display, PNV_KEYBOARD_PACKET packet) { + auto constexpr BUTTON_RELEASED = 0x04; + + platf::keyboard(display, packet->keyCode & 0x00FF, packet->keyAction == BUTTON_RELEASED); +} + +void passthrough(platf::display_t::element_type *display, PNV_SCROLL_PACKET packet) { + platf::scroll(display, util::endian::big(packet->scrollAmt1)); +} + +void passthrough(platf::display_t::element_type *display, void *input) { + int input_type = util::endian::big(*(int*)input); + + switch(input_type) { + case PACKET_TYPE_MOUSE_MOVE: + passthrough(display, (PNV_MOUSE_MOVE_PACKET)input); + break; + case PACKET_TYPE_MOUSE_BUTTON: + passthrough(display, (PNV_MOUSE_BUTTON_PACKET)input); + break; + case PACKET_TYPE_SCROLL_OR_KEYBOARD: + { + char *tmp_input = (char*)input + 4; + if(tmp_input[0] == 0x0A) { + passthrough(display, (PNV_SCROLL_PACKET)input); + } + else { + passthrough(display, (PNV_KEYBOARD_PACKET)input); + } + + break; + } + } +} +} diff --git a/input.h b/input.h new file mode 100644 index 00000000..3232477e --- /dev/null +++ b/input.h @@ -0,0 +1,16 @@ +// +// Created by loki on 6/20/19. +// + +#ifndef SUNSHINE_INPUT_H +#define SUNSHINE_INPUT_H + +#include + +namespace input { +void print(void *input); + +void passthrough(platf::display_t::element_type *display, void *input); +} + +#endif //SUNSHINE_INPUT_H diff --git a/main.cpp b/main.cpp new file mode 100644 index 00000000..7bc36a32 --- /dev/null +++ b/main.cpp @@ -0,0 +1,28 @@ +// +// Created by loki on 5/30/19. +// + +#include +#include + +#include "nvhttp.h" +#include "stream.h" + +extern "C" { +#include +} + +#include + +using namespace std::literals; +int main() { + reed_solomon_init(); + + std::thread httpThread { nvhttp::start }; + std::thread rtpThread { stream::rtpThread }; + + httpThread.join(); + rtpThread.join(); + + return 0; +} diff --git a/moonlight-common-c b/moonlight-common-c new file mode 160000 index 00000000..801aaf43 --- /dev/null +++ b/moonlight-common-c @@ -0,0 +1 @@ +Subproject commit 801aaf43d6124da294a8c97e5b67e966f1b4edbf diff --git a/nvhttp.cpp b/nvhttp.cpp new file mode 100644 index 00000000..2cf80473 --- /dev/null +++ b/nvhttp.cpp @@ -0,0 +1,521 @@ +// +// Created by loki on 6/3/19. +// + +#include "nvhttp.h" + +#include + +#include +#include +#include + +#include + +#include +#include + +#include "uuid.h" +#include "config.h" +#include "utility.h" +#include "stream.h" + +namespace nvhttp { +using namespace std::literals; +constexpr auto PORT_HTTP = 47989; +constexpr auto PORT_HTTPS = 47984; + +constexpr auto VERSION = "7.1.415.0"; +constexpr auto GFE_VERSION = "2.0.0.1"; + +namespace pt = boost::property_tree; + +std::string read_file(const char *path); + +using https_server_t = SimpleWeb::Server; +using http_server_t = SimpleWeb::Server; + +struct conf_intern_t { + std::string servercert; + std::string pkey; +} conf_intern; + +struct client_t { + std::string uniqueID; + std::string cert; +}; + +struct pair_session_t { + client_t client; + + std::unique_ptr cipher_key; + std::vector clienthash; + + std::string serversecret; + std::string serverchallenge; +}; + +// uniqueID, session +std::unordered_map map_id_sess; +std::unordered_map map_id_client; + +using args_t = SimpleWeb::CaseInsensitiveMultimap; + +enum class op_e { + ADD, + REMOVE +}; + +std::string get_pin() { + std::cout << "Please insert PIN: "; + std::string pin; + std::getline(std::cin, pin); + + return pin; +} + +void save_devices() { + pt::ptree root; + + auto &nodes = root.add_child("root.devices", pt::ptree {}); + for(auto &[_,client] : map_id_client) { + pt::ptree node; + + node.put("uniqueid"s, client.uniqueID); + node.put("cert"s, client.cert); + + nodes.push_back(std::make_pair("", node)); + } + + pt::write_json(config::nvhttp.file_devices, root); +} + +void load_devices() { + pt::ptree root; + try { + pt::read_json(config::nvhttp.file_devices, root); + } catch (std::exception &e) { + std::cout << e.what() << std::endl; + + return; + } + + auto nodes = root.get_child("root.devices"); + + for(auto &[_,node] : nodes) { + auto uniqID = node.get("uniqueid"); + auto &client = map_id_client.emplace(uniqID, client_t {}).first->second; + + client.uniqueID = uniqID; + client.cert = node.get("cert"); + } +} + +void update_id_client(client_t &client, op_e op) { + switch(op) { + case op_e::ADD: + { + auto uniqID = client.uniqueID; + map_id_client.emplace(std::move(uniqID), std::move(client)); + } + break; + case op_e::REMOVE: + map_id_client.erase(client.uniqueID); + break; + } + + save_devices(); +} + +void getservercert(pair_session_t &sess, pt::ptree &tree, const args_t &args) { + auto salt = util::from_hex>(args.at("salt"s), true); + + auto pin = get_pin(); + + auto key = crypto::gen_aes_key(*salt, pin); + sess.cipher_key = std::make_unique(key); + + tree.put("root.paired", 1); + tree.put("root.plaincert", util::hex_vec(conf_intern.servercert, true)); + tree.put("root..status_code", 200); +} +void serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const args_t &args) { + auto encrypted_response = util::from_hex_vec(args.at("serverchallengeresp"s), true); + + std::vector decrypted; + crypto::cipher_t cipher(*sess.cipher_key); + cipher.padding = false; + + cipher.decrypt(encrypted_response, decrypted); + + sess.clienthash = std::move(decrypted); + + auto serversecret = sess.serversecret; + auto sign = crypto::sign256(crypto::pkey(conf_intern.pkey), serversecret); + + serversecret.insert(std::end(serversecret), std::begin(sign), std::end(sign)); + + tree.put("root.pairingsecret", util::hex_vec(serversecret, true)); + tree.put("root.paired", 1); + tree.put("root..status_code", 200); +} + +void clientchallenge(pair_session_t &sess, pt::ptree &tree, const args_t &args) { + auto challenge = util::from_hex_vec(args.at("clientchallenge"s), true); + + crypto::cipher_t cipher(*sess.cipher_key); + cipher.padding = false; + + std::vector decrypted; + cipher.decrypt(challenge, decrypted); + + auto x509 = crypto::x509(conf_intern.servercert); + auto sign = crypto::signature(x509); + auto serversecret = crypto::rand(16); + + decrypted.insert(std::end(decrypted), std::begin(sign), std::end(sign)); + decrypted.insert(std::end(decrypted), std::begin(serversecret), std::end(serversecret)); + + auto hash = crypto::hash({ (char*)decrypted.data(), decrypted.size() }); + auto serverchallenge = crypto::rand(16); + + std::string plaintext; + plaintext.reserve(hash.size() + serverchallenge.size()); + + plaintext.insert(std::end(plaintext), std::begin(hash), std::end(hash)); + plaintext.insert(std::end(plaintext), std::begin(serverchallenge), std::end(serverchallenge)); + + std::vector encrypted; + cipher.encrypt(plaintext, encrypted); + + sess.serversecret = std::move(serversecret); + sess.serverchallenge = std::move(serverchallenge); + + tree.put("root.paired", 1); + tree.put("root.challengeresponse", util::hex_vec(encrypted, true)); + tree.put("root..status_code", 200); +} + +void clientpairingsecret(pair_session_t &sess, pt::ptree &tree, const args_t &args) { + auto &client = sess.client; + + auto pairingsecret = util::from_hex_vec(args.at("clientpairingsecret"), true); + + std::string_view secret { pairingsecret.data(), 16 }; + std::string_view sign { pairingsecret.data() + secret.size(), crypto::digest_size }; + + assert((secret.size() + sign.size()) == pairingsecret.size()); + + auto x509 = crypto::x509(sess.client.cert); + auto x509_sign = crypto::signature(x509); + + std::string data; + data.reserve(sess.serverchallenge.size() + x509_sign.size() + secret.size()); + + data.insert(std::end(data), std::begin(sess.serverchallenge), std::end(sess.serverchallenge)); + data.insert(std::end(data), std::begin(x509_sign), std::end(x509_sign)); + data.insert(std::end(data), std::begin(secret), std::end(secret)); + + auto hash = crypto::hash(data); + + // if hash not correct, probably MITM + if(std::memcmp(hash.data(), sess.clienthash.data(), hash.size())) { + //TODO: log + + map_id_sess.erase(client.uniqueID); + tree.put("root.paired", 0); + } + + if(crypto::verify256(crypto::x509(client.cert), secret, sign)) { + tree.put("root.paired", 1); + + auto it = map_id_sess.find(client.uniqueID); + + auto uniqID = client.uniqueID; + update_id_client(client, op_e::ADD); + map_id_sess.erase(it); + } + else { + map_id_sess.erase(client.uniqueID); + tree.put("root.paired", 0); + } + + tree.put("root..status_code", 200); +} + +pt::ptree pair_xml(args_t &&args) { + auto uniqID { std::move(args.at("uniqueid"s)) }; + auto sess_it = map_id_sess.find(uniqID); + + pt::ptree tree; + + args_t::const_iterator it; + if(it = args.find("phrase"); it != std::end(args)) { + if(it->second == "getservercert"sv) { + pair_session_t sess; + + sess.client.uniqueID = std::move(uniqID); + sess.client.cert = util::from_hex_vec(args.at("clientcert"s), true); + + std::cout << sess.client.cert; + + auto ptr = map_id_sess.emplace(sess.client.uniqueID, std::move(sess)).first; + getservercert(ptr->second, tree, args); + } + else if(it->second == "pairchallenge"sv) { + tree.put("root.paired", 1); + tree.put("root..status_code", 200); + } + } + else if(it = args.find("clientchallenge"); it != std::end(args)) { + clientchallenge(sess_it->second, tree, args); + } + else if(it = args.find("serverchallengeresp"); it != std::end(args)) { + serverchallengeresp(sess_it->second, tree, args); + } + else if(it = args.find("clientpairingsecret"); it != std::end(args)) { + clientpairingsecret(sess_it->second, tree, args); + } + else { + tree.put("root..status_code", 404); + } + + return tree; +} + +template +struct tunnel; + +template<> +struct tunnel { + static auto constexpr to_string = "HTTPS"sv; +}; + +template<> +struct tunnel { + static auto constexpr to_string = "NONE"sv; +}; + +template +void print_req(std::shared_ptr::Request> request) { + std::cout << "TUNNEL :: "sv << tunnel::to_string << std::endl; + + std::cout << "METHOD :: "sv << request->method << std::endl; + std::cout << "DESTINATION :: "sv << request->path << std::endl; + + for(auto &[name, val] : request->header) { + std::cout << name << " -- " << val << std::endl; + } + + std::cout << std::endl; + + for(auto &[name, val] : request->parse_query_string()) { + std::cout << name << " -- " << val << std::endl; + } + + std::cout << std::endl; +} + +template +void not_found(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { + print_req(request); + + pt::ptree tree; + tree.put("root..status_code", 404); + + std::ostringstream data; + + pt::write_xml(data, tree); + response->write(data.str()); + + *response << "HTTP/1.1 404 NOT FOUND\r\n" << data.str(); +} + +template +void pair(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { + print_req(request); + + auto tree = pair_xml(request->parse_query_string()); + + std::ostringstream data; + + pt::write_xml(data, tree); + response->write(data.str()); +} + +template +void serverinfo(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { + print_req(request); + + auto args = request->parse_query_string(); + auto clientID = args.find("uniqueid"s); + + int pair_status = 0; + + if(clientID != std::end(args)) { + if (auto it = map_id_client.find(clientID->second); it != std::end(map_id_client)) { + pair_status = 1; + } + } + + pt::ptree tree; + + tree.put("root..status_code", 200); + tree.put("root.hostname", "loki-pc"); + + tree.put("root.appversion", VERSION); + tree.put("root.GfeVersion", GFE_VERSION); + tree.put("root.uniqueid", config::nvhttp.unique_id); + tree.put("root.mac", "42:45:F0:65:D6:F4"); + tree.put("root.LocalIP", "192.168.0.195"); //FIXME: Should be determined at runtime + + if(config::nvhttp.external_ip.empty()) { + tree.put("root.ExternalIP", "192.168.0.195"); + } + else { + tree.put("root.ExternalIP", config::nvhttp.external_ip); + } + + tree.put("root.PairStatus", pair_status); + tree.put("root.currentgame", 0); + tree.put("root.state", "_SERVER_BUSY"); + + std::ostringstream data; + + pt::write_xml(data, tree); + response->write(data.str()); +} + +template +void applist(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { + print_req(request); + + auto args = request->parse_query_string(); + auto clientID = args.at("uniqueid"s); + + pt::ptree tree; + + auto g = util::fail_guard([&]() { + std::ostringstream data; + + pt::write_xml(data, tree); + response->write(data.str()); + }); + + auto client = map_id_client.find(clientID); + if(client == std::end(map_id_client)) { + tree.put("root..status_code", 501); + + return; + } + + auto &apps = tree.add_child("root", pt::ptree {}); + pt::ptree desktop; + pt::ptree fakegame; + + apps.put(".status_code", 200); + desktop.put("IsHdrSupported"s, 0); + desktop.put("AppTitle"s, "Desktop"); + desktop.put("ID"s, 1); + + fakegame.put("IsHdrSupported"s, 0); + fakegame.put("AppTitle"s, "FakeGame"); + fakegame.put("ID"s, 2); + + apps.push_back(std::make_pair("App", desktop)); + apps.push_back(std::make_pair("App", fakegame)); +} + +template +void launch(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { + print_req(request); + + auto args = request->parse_query_string(); + auto clientID = args.at("uniqueid"s); + auto aesKey = *util::from_hex(args.at("rikey"s), true); + uint32_t prepend_iv = util::endian::big(util::from_view(args.at("rikeyid"s))); + auto prepend_iv_p = (uint8_t*)&prepend_iv; + + std::copy(std::begin(aesKey), std::end(aesKey), std::begin(stream::gcm_key)); + auto next = std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(stream::iv)); + std::fill(next, std::end(stream::iv), 0); + + pt::ptree tree; + + auto g = util::fail_guard([&]() { + std::ostringstream data; + + pt::write_xml(data, tree); + response->write(data.str()); + }); + +/* + bool sops = args.at("sops"s) == "1"; + std::optional gcmap { std::nullopt }; + if(auto it = args.find("gcmap"s); it != std::end(args)) { + gcmap = std::stoi(it->second); + } +*/ + + tree.put("root..status_code", 200); + tree.put("root.gamesession", 1); +} + +template +void appasset(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { + std::ifstream in(SUNSHINE_ASSETS_DIR "/box.png"); + response->write(SimpleWeb::StatusCode::success_ok, in); +} + +void start() { + load_devices(); + + conf_intern.pkey = read_file(config::nvhttp.pkey.c_str()); + conf_intern.servercert = read_file(config::nvhttp.cert.c_str()); + + https_server_t https_server { config::nvhttp.cert, config::nvhttp.pkey }; + http_server_t http_server; + + https_server.default_resource = not_found; + https_server.resource["^/serverinfo"]["GET"] = serverinfo; + https_server.resource["^/pair"]["GET"] = pair; + https_server.resource["^/applist"]["GET"] = applist; + https_server.resource["^/appasset"]["GET"] = appasset; + https_server.resource["^/launch"]["GET"] = launch; + + https_server.config.reuse_address = true; + https_server.config.address = "0.0.0.0"s; + https_server.config.port = PORT_HTTPS; + + http_server.default_resource = not_found; + http_server.resource["^/serverinfo"]["GET"] = serverinfo; + http_server.resource["^/pair"]["GET"] = pair; + http_server.resource["^/applist"]["GET"] = applist; + http_server.resource["^/appasset"]["GET"] = appasset; + http_server.resource["^/launch"]["GET"] = launch; + + http_server.config.reuse_address = true; + http_server.config.address = "0.0.0.0"s; + http_server.config.port = PORT_HTTP; + + std::thread ssl { &https_server_t::start, &https_server }; + std::thread tcp { &http_server_t::start, &http_server }; + + ssl.join(); + tcp.join(); +} + +std::string read_file(const char *path) { + std::ifstream in(path); + + std::string input; + std::string base64_cert; + + while(!in.eof()) { + std::getline(in, input); + base64_cert += input + '\n'; + } + + return base64_cert; +} +} diff --git a/nvhttp.h b/nvhttp.h new file mode 100644 index 00000000..50bd0a0b --- /dev/null +++ b/nvhttp.h @@ -0,0 +1,19 @@ +// +// Created by loki on 6/3/19. +// + +#ifndef SUNSHINE_NVHTTP_H +#define SUNSHINE_NVHTTP_H + +#include +#include + +#define CA_DIR SUNSHINE_ASSETS_DIR "/demoCA" +#define PRIVATE_KEY_FILE CA_DIR "/cakey.pem" +#define CERTIFICATE_FILE CA_DIR "/cacert.pem" + +namespace nvhttp { +void start(); +} + +#endif //SUNSHINE_NVHTTP_H diff --git a/platform/common.h b/platform/common.h new file mode 100644 index 00000000..c7002c6f --- /dev/null +++ b/platform/common.h @@ -0,0 +1,40 @@ +// +// Created by loki on 6/21/19. +// + +#ifndef SUNSHINE_COMMON_H +#define SUNSHINE_COMMON_H + +#include + +namespace platf { + +void freeDisplay(void*); +void freeImage(void*); +void freeAudio(void*); +void freeMic(void*); + +using display_t = util::safe_ptr; +using img_t = util::safe_ptr; +using mic_t = util::safe_ptr; +using audio_t = util::safe_ptr; + +display_t display(); +img_t snapshot(display_t &display); +mic_t microphone(); +audio_t audio(mic_t &mic, std::uint32_t sample_size); + +int32_t img_width(img_t &); +int32_t img_height(img_t &); + +uint8_t *img_data(img_t &); +int16_t *audio_data(audio_t &); + +void move_mouse(display_t::element_type *display, int deltaX, int deltaY); +void button_mouse(display_t::element_type *display, int button, bool release); +void scroll(display_t::element_type *display, int distance); +void keyboard(display_t::element_type *display, uint16_t modcode, bool release); + +} + +#endif //SUNSHINE_COMMON_H diff --git a/platform/linux.cpp b/platform/linux.cpp new file mode 100644 index 00000000..0680d52f --- /dev/null +++ b/platform/linux.cpp @@ -0,0 +1,314 @@ +// +// Created by loki on 6/21/19. +// + +#include "common.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +namespace platf { +using namespace std::literals; +struct display_attr_t { + display_attr_t() : display { XOpenDisplay(nullptr) }, window { DefaultRootWindow(display) }, attr {} { + XGetWindowAttributes(display, window, &attr); + } + + ~display_attr_t() { + XCloseDisplay(display); + } + + Display *display; + Window window; + XWindowAttributes attr; +}; + +struct mic_attr_t { + pa_sample_spec ss; + util::safe_ptr mic; +}; + +display_t display() { + return display_t { new display_attr_t {} }; +} + +img_t snapshot(display_t &display_void) { + auto &display = *((display_attr_t*)display_void.get()); + + XImage *img { XGetImage( + display.display, + display.window, + 0, 0, + display.attr.width, display.attr.height, + AllPlanes, ZPixmap) + }; + + XFixesCursorImage *overlay = XFixesGetCursorImage(display.display); + + auto pixels = (int*)img->data; + + auto screen_height = display.attr.height; + auto screen_width = display.attr.width; + + auto delta_height = std::min(overlay->height, std::abs(overlay->y - screen_height)); + auto delta_width = std::min(overlay->width, std::abs(overlay->x - screen_width)); + for(auto y = 0; y < delta_height; ++y) { + + auto overlay_begin = &overlay->pixels[y * overlay->width]; + auto overlay_end = &overlay->pixels[y * overlay->width + delta_width]; + + auto pixels_begin = &pixels[(y + overlay->y - 1) * screen_width + overlay->x - 1]; + std::for_each(overlay_begin, overlay_end, [&](long pixel) { + int *pixel_p = (int*)&pixel; + + if(pixel_p[0] != 0) { + *pixels_begin = pixel_p[0]; + } + ++pixels_begin; + }); + } + + return img_t { img }; +} + +uint8_t *img_data(img_t &img) { + return (uint8_t*)((XImage*)img.get())->data; +} + +int32_t img_width(img_t &img) { + return ((XImage*)img.get())->width; +} + +int32_t img_height(img_t &img) { + return ((XImage*)img.get())->height; +} + +//FIXME: Pass frame_rate instead of hard coding it +mic_t microphone() { + mic_t mic { + new mic_attr_t { + { PA_SAMPLE_S16LE, 48000, 2 }, + { } + } + }; + + int error; + mic_attr_t *mic_attr = (mic_attr_t*)mic.get(); + mic_attr->mic.reset( + pa_simple_new(nullptr, "sunshine", pa_stream_direction_t::PA_STREAM_RECORD, nullptr, "sunshine_record", &mic_attr->ss, nullptr, nullptr, &error) + ); + + if(!mic_attr->mic) { + auto err_str = pa_strerror(error); + std::cout << "pa_simple_new() failed: "sv << err_str << std::endl; + + exit(1); + } + + return mic; +} + +audio_t audio(mic_t &mic, std::uint32_t buf_size) { + auto mic_attr = (mic_attr_t*)mic.get(); + + audio_t result { new std::uint8_t[buf_size] }; + + auto buf = (std::uint8_t*)result.get(); + int error; + if(pa_simple_read(mic_attr->mic.get(), buf, buf_size, &error)) { + std::cout << "pa_simple_read() failed: "sv << pa_strerror(error) << std::endl; + } + + return result; +} + +std::int16_t *audio_data(audio_t &audio) { + return (int16_t*)audio.get(); +} + + +void move_mouse(display_t::element_type *display, int deltaX, int deltaY) { + auto &disp = *((display_attr_t*)display); + + XWarpPointer(disp.display, None, None, 0, 0, 0, 0, deltaX, deltaY); + XFlush(disp.display); +} + +void button_mouse(display_t::element_type *display, int button, bool release) { + auto &disp = *((display_attr_t *) display); + + XTestFakeButtonEvent(disp.display, button, !release, CurrentTime); + + XFlush(disp.display); +} + +void scroll(display_t::element_type *display, int distance) { + auto &disp = *((display_attr_t *) display); + + int button = distance > 0 ? 4 : 5; + + distance = std::abs(distance / 120); + while(distance > 0) { + --distance; + + XTestFakeButtonEvent(disp.display, button, True, CurrentTime); + XTestFakeButtonEvent(disp.display, button, False, CurrentTime); + + XSync(disp.display, 0); + } + + XFlush(disp.display); +} + +uint16_t keysym(uint16_t modcode) { + constexpr auto VK_NUMPAD = 0x60; + constexpr auto VK_F1 = 0x70; + + if(modcode >= VK_NUMPAD && modcode < VK_NUMPAD + 10) { + return XK_KP_0 + (modcode - VK_NUMPAD); + } + + if(modcode >= VK_F1 && modcode < VK_F1 + 13) { + return XK_F1 + (modcode - VK_F1); + } + + + switch(modcode) { + case 0x08: + return XK_BackSpace; + case 0x09: + return XK_Tab; + case 0x0D: + return XK_Return; + case 0x13: + return XK_Pause; + case 0x14: + return XK_Caps_Lock; + case 0x1B: + return XK_Escape; + case 0x21: + return XK_Page_Up; + case 0x22: + return XK_Page_Down; + case 0x23: + return XK_End; + case 0x24: + return XK_Home; + case 0x25: + return XK_Left; + case 0x26: + return XK_Up; + case 0x27: + return XK_Right; + case 0x28: + return XK_Down; + case 0x29: + return XK_Select; + case 0x2B: + return XK_Execute; + case 0x2C: + return XK_Print; //FIXME: is this correct? (printscreen) + case 0x2D: + return XK_Insert; + case 0x2E: + return XK_Delete; + case 0x2F: + return XK_Help; + case 0x6A: + return XK_KP_Multiply; + case 0x6B: + return XK_KP_Add; + case 0x6C: + return XK_KP_Decimal; //FIXME: is this correct? (Comma) + case 0x6D: + return XK_KP_Subtract; + case 0x6E: + return XK_KP_Separator; //FIXME: is this correct? (Period) + case 0x6F: + return XK_KP_Divide; + case 0x90: + return XK_Num_Lock; //FIXME: is this correct: (NumlockClear) + case 0x91: + return XK_Scroll_Lock; + case 0xA0: + return XK_Shift_L; + case 0xA1: + return XK_Shift_R; + case 0xA2: + return XK_Control_L; + case 0xA3: + return XK_Control_R; + case 0xA4: + return XK_Alt_L; + case 0xA5: /* return XK_Alt_R; */ + return XK_Super_L; + case 0xBA: + return XK_semicolon; + case 0xBB: + return XK_equal; + case 0xBC: + return XK_comma; + case 0xBD: + return XK_minus; + case 0xBE: + return XK_period; + case 0xBF: + return XK_slash; + case 0xC0: + return XK_grave; + case 0xDB: + return XK_bracketleft; + case 0xDC: + return XK_backslash; + case 0xDD: + return XK_bracketright; + case 0xDE: + return XK_apostrophe; + case 0x01: //FIXME: Moonlight doesn't support Super key + return XK_Super_L; + case 0x02: + return XK_Super_R; + } + + return modcode; +} + +void keyboard(display_t::element_type *display, uint16_t modcode, bool release) { + auto &disp = *((display_attr_t *) display); + KeyCode kc = XKeysymToKeycode(disp.display, keysym(modcode)); + + if(!kc) { + return; + } + + XTestFakeKeyEvent(disp.display, kc, !release, 0); + + XSync(disp.display, 0); + XFlush(disp.display); +} + +void freeDisplay(void*p) { + delete (display_attr_t*)p; +} + +void freeImage(void*p) { + XDestroyImage((XImage*)p); +} + +void freeMic(void*p) { + delete (mic_attr_t*)p; +} + +void freeAudio(void*p) { + delete[] (std::uint8_t*)p; +} +} diff --git a/queue.h b/queue.h new file mode 100644 index 00000000..c8525adb --- /dev/null +++ b/queue.h @@ -0,0 +1,87 @@ +// +// Created by loki on 6/10/19. +// + +#ifndef SUNSHINE_QUEUE_H +#define SUNSHINE_QUEUE_H + +#include +#include +#include + +#include "utility.h" + +namespace safe { + +template +class queue_t { + using status_t = util::either_t< + (std::is_same_v || + util::instantiation_of_v || + util::instantiation_of_v || + std::is_pointer_v), + T, std::optional>; + +public: + template + void push(Args &&... args) { + std::lock_guard lg{_lock}; + + if(!_continue) { + return; + } + + _queue.emplace_back(std::forward(args)...); + + _cv.notify_all(); + } + + status_t pop() { + std::unique_lock ul{_lock}; + + if (!_continue) { + return util::false_v; + } + + while (_queue.empty()) { + _cv.wait(ul); + + if (!_continue) { + return util::false_v; + } + } + + auto val = std::move(_queue.front()); + _queue.erase(std::begin(_queue)); + + return val; + } + + std::vector &unsafe() { + return _queue; + } + + void stop() { + std::lock_guard lg{_lock}; + + _continue = false; + + _cv.notify_all(); + } + + bool running() const { + return _continue; + } + +private: + + bool _continue{true}; + + std::mutex _lock; + std::condition_variable _cv; + std::vector _queue; +}; + +} + +#endif //SUNSHINE_QUEUE_H diff --git a/stream.cpp b/stream.cpp new file mode 100644 index 00000000..3a9aed3d --- /dev/null +++ b/stream.cpp @@ -0,0 +1,864 @@ +// +// Created by loki on 6/5/19. +// + +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +#include +#include +} + +#include "config.h" +#include "utility.h" +#include "stream.h" +#include "audio.h" +#include "video.h" +#include "queue.h" +#include "crypto.h" +#include "input.h" + +#define IDX_START_A 0 +#define IDX_REQUEST_IDR_FRAME 0 +#define IDX_START_B 1 +#define IDX_INVALIDATE_REF_FRAMES 2 +#define IDX_LOSS_STATS 3 +#define IDX_INPUT_DATA 5 +#define IDX_RUMBLE_DATA 6 +#define IDX_TERMINATION 7 + +static const short packetTypes[] = { + 0x0305, // Start A + 0x0307, // Start B + 0x0301, // Invalidate reference frames + 0x0201, // Loss Stats + 0x0204, // Frame Stats (unused) + 0x0206, // Input data + 0x010b, // Rumble data + 0x0100, // Termination +}; + +namespace asio = boost::asio; +namespace sys = boost::system; + +using asio::ip::tcp; +using asio::ip::udp; + +using namespace std::literals; + +namespace stream { + +constexpr auto RTSP_SETUP_PORT = 48010; +constexpr auto VIDEO_STREAM_PORT = 47998; +constexpr auto CONTROL_PORT = 47999; +constexpr auto AUDIO_STREAM_PORT = 48000; + +#pragma pack(push, 1) + +struct video_packet_raw_t { + uint8_t *payload() { + return (uint8_t *)(this + 1); + } + + RTP_PACKET rtp; + NV_VIDEO_PACKET packet; +}; + +struct audio_packet_raw_t { + uint8_t *payload() { + return (uint8_t *)(this + 1); + } + + RTP_PACKET rtp; +}; + +#pragma pack(pop) + +crypto::aes_t gcm_key; +crypto::aes_t iv; + +struct config_t { + audio::config_t audio; + video::config_t monitor; + int packetsize; + + bool sops; + std::optional gcmap; +}; + +struct session_t { + config_t config; + + std::thread audioThread; + std::thread videoThread; + std::thread controlThread; + + std::chrono::steady_clock::time_point pingTimeout; + int client_state; + + crypto::aes_t gcm_key; + crypto::aes_t iv; +} session; + +void free_msg(PRTSP_MESSAGE msg) { + freeMessage(msg); + + delete msg; +} + +using msg_t = util::safe_ptr; +using packet_t = util::safe_ptr; +using host_t = util::safe_ptr; +using rh_t = util::safe_ptr; +using video_packet_t = util::safe_ptr; +using audio_packet_t = util::safe_ptr; + +host_t host_create(ENetAddress &addr, std::uint16_t port) { + enet_address_set_host(&addr, "0.0.0.0"); + enet_address_set_port(&addr, port); + + return host_t { enet_host_create(PF_INET, &addr, 1, 1, 0, 0) }; +} + +class server_t { +public: + server_t(server_t &&) noexcept = default; + server_t &operator=(server_t &&) noexcept = default; + + explicit server_t(std::uint16_t port) : _host { host_create(_addr, port) } {} + + template + void iterate(std::chrono::duration timeout) { + ENetEvent event; + auto res = enet_host_service(_host.get(), &event, std::chrono::floor(timeout).count()); + + if(res > 0) { + switch(event.type) { + case ENET_EVENT_TYPE_RECEIVE: + { + packet_t packet { event.packet }; + + std::uint16_t *type = (std::uint16_t *)packet->data; + std::string_view payload { (char*)packet->data + sizeof(*type), packet->dataLength - sizeof(*type) }; + + + auto cb = _map_type_cb.find(*type); + if(cb == std::end(_map_type_cb)) { + std::cout << "type [Unknown] { " << util::hex(*type).to_string_view() << " }" << std::endl; + std::cout << "---data---" << std::endl << util::hex_vec(payload) << std::endl << "---end data---" << std::endl; + } + + else { + cb->second(payload); + } + } + break; + case ENET_EVENT_TYPE_CONNECT: + std::cout << "CLIENT CONNECTED" << std::endl; + break; + case ENET_EVENT_TYPE_DISCONNECT: + std::cout << "CLIENT DISCONNECTED" << std::endl; + break; + case ENET_EVENT_TYPE_NONE: + break; + } + } + } + void map(uint16_t type, std::function cb); +private: + std::unordered_map> _map_type_cb; + ENetAddress _addr; + host_t _host; +}; + +namespace fec { +using rs_t = util::safe_ptr; + +struct fec_t { + size_t data_shards; + size_t nr_shards; + size_t percentage; + + size_t blocksize; + util::buffer_t shards; + + std::string_view operator[](size_t el) const { + return { &shards[el*blocksize], blocksize }; + } + + size_t size() const { + return nr_shards; + } +}; + +fec_t encode(const std::string_view &payload, size_t blocksize, size_t fecpercentage) { + auto payload_size = payload.size(); + + auto pad = payload_size % blocksize != 0; + + auto data_shards = payload_size / blocksize + (pad ? 1 : 0); + auto parity_shards = (data_shards * fecpercentage + 99) / 100; + auto nr_shards = data_shards + parity_shards; + + if(nr_shards > DATA_SHARDS_MAX) { + std::cerr << "Error: number of fragments for reed solomon exceeds DATA_SHARDS_MAX"sv << std::endl; + std::cerr << nr_shards << " > "sv << DATA_SHARDS_MAX << std::endl; + exit(9); + } + + util::buffer_t shards { nr_shards * blocksize }; + util::buffer_t shards_p { nr_shards }; + + // copy payload + padding + auto next = std::copy(std::begin(payload), std::end(payload), std::begin(shards)); + std::fill(next, std::end(shards), 0); // padding with zero + + for(auto x = 0; x < nr_shards; ++x) { + shards_p[x] = (uint8_t*)&shards[x * blocksize]; + } + + // packets = parity_shards + data_shards + rs_t rs { reed_solomon_new(data_shards, parity_shards) }; + + reed_solomon_encode(rs.get(), shards_p.begin(), nr_shards, blocksize); + + return { + data_shards, + nr_shards, + fecpercentage, + blocksize, + std::move(shards) + }; +} +} + +template +std::vector insert(uint64_t insert_size, uint64_t slice_size, const std::string_view &data, F &&f) { + auto pad = data.size() % slice_size != 0; + auto elements = data.size() / slice_size + (pad ? 1 : 0); + + std::vector result; + result.resize(elements * insert_size + data.size()); + + auto next = std::begin(data); + for(auto x = 0; x < elements - 1; ++x) { + void *p = &result[x*(insert_size + slice_size)]; + + f(p, x, elements); + + std::copy(next, next + slice_size, (char*)p + insert_size); + next += slice_size; + } + + if(pad) { + auto x = elements - 1; + void *p = &result[x*(insert_size + slice_size)]; + + f(p, x, elements); + + std::copy(next, std::end(data), (char*)p + insert_size); + } + + return result; +} + +void print_msg(PRTSP_MESSAGE msg) { + std::string_view type = msg->type == TYPE_RESPONSE ? "RESPONSE"sv : "REQUEST"sv; + + std::string_view payload { msg->payload, (size_t)msg->payloadLength }; + std::string_view protocol { msg->protocol }; + auto seqnm = msg->sequenceNumber; + std::string_view messageBuffer { msg->messageBuffer }; + + std::cout << "type ["sv << type << ']' << std::endl; + std::cout << "sequence number ["sv << seqnm << ']' << std::endl; + std::cout << "protocol :: "sv << protocol << std::endl; + std::cout << "payload :: "sv << payload << std::endl; + + if(msg->type == TYPE_RESPONSE) { + auto &resp = msg->message.response; + + auto statuscode = resp.statusCode; + std::string_view status { resp.statusString }; + + std::cout << "statuscode :: "sv << statuscode << std::endl; + std::cout << "status :: "sv << status << std::endl; + } + else { + auto& req = msg->message.request; + + std::string_view command { req.command }; + std::string_view target { req.target }; + + std::cout << "command :: "sv << command << std::endl; + std::cout << "target :: "sv << target << std::endl; + } + + for(auto option = msg->options; option != nullptr; option = option->next) { + std::string_view content { option->content }; + std::string_view name { option->option }; + + std::cout << name << " :: "sv << content << std::endl; + } + + std::cout << "---Begin MessageBuffer---"sv << std::endl << messageBuffer << std::endl << "---End MessageBuffer---"sv << std::endl << std::endl; +} + +using frame_queue_t = std::vector; +video::packet_t next_packet(uint16_t &frame, std::shared_ptr> &packets, frame_queue_t &packet_queue) { + auto packet = packets->pop(); + + if(!packet) { + return nullptr; + } + + assert(packet->pts >= frame); + + auto comp = [](const video::packet_t &l, const video::packet_t &r) { + return l->pts > r->pts; + }; + + if(packet->pts > frame) { + packet_queue.emplace_back(std::move(packet)); + std::push_heap(std::begin(packet_queue), std::end(packet_queue), comp); + + if (packet_queue.front()->pts != frame) { + return next_packet(frame, packets, packet_queue); + } + + std::pop_heap(std::begin(packet_queue), std::end(packet_queue), comp); + packet = std::move(packet_queue.back()); + packet_queue.pop_back(); + } + + ++frame; + return packet; +} + +std::vector replace(const std::string_view &original, const std::string_view &old, const std::string_view &_new) { + std::vector replaced; + + auto search = [&](auto it) { + return std::search(it, std::end(original), std::begin(old), std::end(old)); + }; + + auto begin = std::begin(original); + for(auto next = search(begin); next != std::end(original); next = search(++next)) { + std::copy(begin, next, std::back_inserter(replaced)); + std::copy(std::begin(_new), std::end(_new), std::back_inserter(replaced)); + + next = begin = next + old.size(); + } + + std::copy(begin, std::end(original), std::back_inserter(replaced)); + + return replaced; +} + +void server_t::map(uint16_t type, std::function cb) { + _map_type_cb.emplace(type, std::move(cb)); +} + +void controlThread() { + server_t server { CONTROL_PORT }; + + std::shared_ptr display = platf::display(); + server.map(packetTypes[IDX_START_A], [](const std::string_view &payload) { + session.pingTimeout = std::chrono::steady_clock::now() + config::stream.ping_timeout; + + std::cout << "type [IDX_START_A]"sv << std::endl; + }); + + server.map(packetTypes[IDX_START_B], [](const std::string_view &payload) { + session.pingTimeout = std::chrono::steady_clock::now() + config::stream.ping_timeout; + + std::cout << "type [IDX_START_B]"sv << std::endl; + }); + + server.map(packetTypes[IDX_LOSS_STATS], [](const std::string_view &payload) { + session.pingTimeout = std::chrono::steady_clock::now() + config::stream.ping_timeout; + +/* std::cout << "type [IDX_LOSS_STATS]"sv << std::endl; + + int32_t *stats = (int32_t*)payload.data(); + auto count = stats[0]; + std::chrono::milliseconds t { stats[1] }; + + auto lastGoodFrame = stats[3]; + + std::cout << "---begin stats---" << std::endl; + std::cout << "loss count since last report [" << count << ']' << std::endl; + std::cout << "time in milli since last report [" << t.count() << ']' << std::endl; + std::cout << "last good frame [" << lastGoodFrame << ']' << std::endl; + std::cout << "---end stats---" << std::endl; */ + }); + + server.map(packetTypes[IDX_INVALIDATE_REF_FRAMES], [](const std::string_view &payload) { + session.pingTimeout = std::chrono::steady_clock::now() + config::stream.ping_timeout; + + std::cout << "type [IDX_INVALIDATE_REF_FRAMES]"sv << std::endl; + + std::int64_t *frames = (std::int64_t *)payload.data(); + auto firstFrame = frames[0]; + auto lastFrame = frames[1]; + + std::cout << "firstFrame [" << firstFrame << ']' << std::endl; + std::cout << "lastFrame [" << lastFrame << ']' << std::endl; + }); + + server.map(packetTypes[IDX_INPUT_DATA], [display](const std::string_view &payload) mutable { + session.pingTimeout = std::chrono::steady_clock::now() + config::stream.ping_timeout; + + std::cout << "type [IDX_INPUT_DATA]"sv << std::endl; + + int32_t tagged_cipher_length = util::endian::big(*(int32_t*)payload.data()); + std::string_view tagged_cipher { payload.data() + sizeof(tagged_cipher_length), (size_t)tagged_cipher_length }; + + crypto::cipher_t cipher { session.gcm_key }; + cipher.padding = false; + + std::vector plaintext; + if(cipher.decrypt_gcm(session.iv, tagged_cipher, plaintext)) { + // something went wrong :( + + std::cout << "failed to verify tag"sv << std::endl; + session.client_state = 0; + } + + if(tagged_cipher_length >= 16 + session.iv.size()) { + std::copy(payload.end() - 16, payload.end(), std::begin(session.iv)); + } + + input::print(plaintext.data()); + input::passthrough(display.get(), plaintext.data()); + }); + + while(session.client_state > 0) { + if(std::chrono::steady_clock::now() > session.pingTimeout) { + session.client_state = 0; + } + + server.iterate(2s); + } +} + +std::optional recv_peer(udp::socket &sock) { + std::array buf; + + char ping[] = { + 0x50, 0x49, 0x4E, 0x47 + }; + + udp::endpoint peer; + while (session.client_state > 0) { + asio::deadline_timer timer { sock.get_executor() }; + timer.expires_from_now(boost::posix_time::seconds(2)); + timer.async_wait([&](sys::error_code c){ + sock.cancel(); + }); + + sys::error_code ping_error; + auto len = sock.receive_from(asio::buffer(buf), peer, 0, ping_error); + if(ping_error == sys::errc::make_error_code(sys::errc::operation_canceled)) { + return {}; + } + + timer.cancel(); + + if (len == 4 && !std::memcmp(ping, buf.data(), sizeof(ping))) { + std::cout << "PING from ["sv << peer.address().to_string() << ':' << peer.port() << ']' << std::endl; + + return std::make_optional(std::move(peer));; + } + + std::cout << "Unknown transmission: "sv << util::hex_vec(std::string_view{buf.data(), len}) << std::endl; + } + + return {}; +} + +void audioThread() { + auto &config = session.config; + + asio::io_service io; + udp::socket sock{io, udp::endpoint(udp::v6(), AUDIO_STREAM_PORT)}; + + auto peer = recv_peer(sock); + if(!peer) { + return; + } + + std::shared_ptr> packets{new safe::queue_t}; + + std::thread captureThread{audio::capture, packets, config.audio}; + + uint16_t frame{1}; + + while (auto packet = packets->pop()) { + if(session.client_state == 0) { + packets->stop(); + + break; + } + + audio_packet_t audio_packet { (audio_packet_raw_t*)malloc(sizeof(audio_packet_raw_t) + packet->size()) }; + + audio_packet->rtp.sequenceNumber = util::endian::big(frame++); + audio_packet->rtp.packetType = 97; + std::copy(std::begin(*packet), std::end(*packet), audio_packet->payload()); + + sock.send_to(asio::buffer((char*)audio_packet.get(), sizeof(audio_packet_raw_t) + packet->size()), *peer); + // std::cout << "Audio ["sv << frame << "] :: send..."sv << std::endl; + } + + captureThread.join(); +} + +void videoThread() { + auto &config = session.config; + + int lowseq = 0; + + asio::io_service io; + udp::socket sock{io, udp::endpoint(udp::v6(), VIDEO_STREAM_PORT)}; + + auto peer = recv_peer(sock); + if(!peer) { + return; + } + + std::shared_ptr> packets{new safe::queue_t}; + + std::thread captureThread{video::capture_display, packets, config.monitor}; + + frame_queue_t packet_queue; + uint16_t frame{1}; + + while (auto packet = next_packet(frame, packets, packet_queue)) { + if(session.client_state == 0) { + packets->stop(); + + break; + } + + std::string_view payload{(char *) packet->data, (size_t) packet->size}; + std::vector payload_new; + + auto nv_packet_header = "\0017charss"sv; + std::copy(std::begin(nv_packet_header), std::end(nv_packet_header), std::back_inserter(payload_new)); + std::copy(std::begin(payload), std::end(payload), std::back_inserter(payload_new)); + + payload = {(char *) payload_new.data(), payload_new.size()}; + + // make sure moonlight recognizes the nalu code for IDR frames + if (packet->flags & AV_PKT_FLAG_KEY) { + //TODO: Not all encoders encode their IDR frames with `"\000\000\001e"` + auto seq_i_frame_old = "\000\000\001e"sv; + auto seq_i_frame = "\000\000\000\001e"sv; + + assert(std::search(std::begin(payload), std::end(payload), std::begin(seq_i_frame), std::end(seq_i_frame)) == + std::end(payload)); + payload_new = replace(payload, seq_i_frame_old, seq_i_frame); + + payload = {(char *) payload_new.data(), payload_new.size()}; + } + + // insert packet headers + auto blocksize = config.packetsize + MAX_RTP_HEADER_SIZE; + auto payload_blocksize = blocksize - sizeof(video_packet_raw_t); + + auto fecpercentage { 25 }; + + payload_new = insert(sizeof(video_packet_raw_t), payload_blocksize, + payload, [&](void *p, int fecIndex, int end) { + video_packet_raw_t *video_packet = (video_packet_raw_t *)p; + + video_packet->packet.flags = FLAG_CONTAINS_PIC_DATA; + video_packet->packet.frameIndex = packet->pts; + video_packet->packet.streamPacketIndex = ((uint32_t)lowseq + fecIndex) << 8; + video_packet->packet.fecInfo = ( + fecIndex << 12 | + end << 22 | + fecpercentage << 4 + ); + + if(fecIndex == 0) { + video_packet->packet.flags |= FLAG_SOF; + } + + if(fecIndex == end - 1) { + video_packet->packet.flags |= FLAG_EOF; + } + + video_packet->rtp.sequenceNumber = util::endian::big(lowseq + fecIndex); + }); + + payload = {(char *) payload_new.data(), payload_new.size()}; + + auto shards = fec::encode(payload, blocksize, 25); + + for (auto x = shards.data_shards; x < shards.size(); ++x) { + video_packet_raw_t *inspect = (video_packet_raw_t *)shards[x].data(); + + inspect->packet.flags = FLAG_CONTAINS_PIC_DATA; + inspect->packet.streamPacketIndex = ((uint32_t)(lowseq + x)) << 8; + inspect->packet.frameIndex = packet->pts; + inspect->packet.fecInfo = ( + x << 12 | + shards.data_shards << 22 | + fecpercentage << 4 + ); + + inspect->rtp.sequenceNumber = util::endian::big(lowseq + x); + } + + for (auto x = 0; x < shards.size(); ++x) { + sock.send_to(asio::buffer(shards[x]), *peer); + } + + // std::cout << "Frame ["sv << packet->pts << "] :: send ["sv << shards.size() << "] shards..."sv << std::endl; + lowseq += shards.size(); + + } + + captureThread.join(); +} + +void respond(tcp::socket &sock, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload) { + RTSP_MESSAGE resp {}; + + auto g = util::fail_guard([&]() { + freeMessage(&resp); + }); + + createRtspResponse(&resp, nullptr, 0, const_cast("RTSP/1.0"), statuscode, const_cast(status_msg), seqn, options, const_cast(payload.data()), (int)payload.size()); + + int serialized_len; + util::c_ptr raw_resp { serializeRtspMessage(&resp, &serialized_len) }; + + std::string_view tmp_resp { raw_resp.get(), (size_t)serialized_len }; + std::cout << "---Begin Response---" << std::endl << tmp_resp << "---End Response---" << std::endl << std::endl; + + asio::write(sock, asio::buffer(tmp_resp)); +} + +void cmd_not_found(tcp::socket &&sock, msg_t&& req) { + respond(sock, nullptr, 404, "NOT FOUND", req->sequenceNumber, {}); +} + +void cmd_option(tcp::socket &&sock, msg_t&& req) { + OPTION_ITEM option {}; + + // I know these string literals will not be modified + option.option = const_cast("CSeq"); + + auto seqn_str = std::to_string(req->sequenceNumber); + option.content = const_cast(seqn_str.c_str()); + + respond(sock, &option, 200, "OK", req->sequenceNumber, {}); +} + +void cmd_describe(tcp::socket &&sock, msg_t&& req) { + OPTION_ITEM option {}; + + // I know these string literals will not be modified + option.option = const_cast("CSeq"); + + auto seqn_str = std::to_string(req->sequenceNumber); + option.content = const_cast(seqn_str.c_str()); + + // FIXME: Moonlight will accept the payload, but the value of the option is not correct + respond(sock, &option, 200, "OK", req->sequenceNumber, "surround-params=NONE"sv); +} + +void cmd_setup(tcp::socket &&sock, msg_t &&req) { + OPTION_ITEM options[2] {}; + + auto &seqn = options[0]; + auto &session_option = options[1]; + + seqn.option = const_cast("CSeq"); + + auto seqn_str = std::to_string(req->sequenceNumber); + seqn.content = const_cast(seqn_str.c_str()); + + if(session.client_state >= 0) { + // already streaming + + respond(sock, &seqn, 503, "Service Unavailable", req->sequenceNumber, {}); + return; + } + + std::string_view target { req->message.request.target }; + auto begin = std::find(std::begin(target), std::end(target), '=') + 1; + auto end = std::find(begin, std::end(target), '/'); + std::string_view type { begin, (size_t)std::distance(begin, end) }; + + if(type == "audio"sv) { + seqn.next = &session_option; + + session_option.option = const_cast("Session"); + session_option.content = const_cast("DEADBEEFCAFE;timeout = 90"); + } + else if(type != "video"sv && type != "control"sv) { + cmd_not_found(std::move(sock), std::move(req)); + + return; + } + + respond(sock, &seqn, 200, "OK", req->sequenceNumber, {}); +} + +void cmd_announce(tcp::socket &&sock, msg_t &&req) { + OPTION_ITEM option {}; + + // I know these string literals will not be modified + option.option = const_cast("CSeq"); + + auto seqn_str = std::to_string(req->sequenceNumber); + option.content = const_cast(seqn_str.c_str()); + + if(session.client_state >= 0) { + // already streaming + + respond(sock, &option, 503, "Service Unavailable", req->sequenceNumber, {}); + return; + } + + std::string_view payload { req->payload, (size_t)req->payloadLength }; + std::vector lines; + + auto whitespace = [](char ch) { + return ch == '\n' || ch == '\r'; + }; + + { + auto pos = std::begin(payload); + auto begin = pos; + while (pos != std::end(payload)) { + if (whitespace(*pos++)) { + lines.emplace_back(begin, pos - begin - 1); + + while(whitespace(*pos)) { ++pos; } + begin = pos; + } + } + } + + std::string_view client; + std::unordered_map args; + + for(auto line : lines) { + auto type = line.substr(0, 2); + if(type == "s="sv) { + client = line.substr(2); + } + else if(type == "a=") { + auto pos = line.find(':'); + + auto name = line.substr(2, pos - 2); + auto val = line.substr(pos + 1); + + if(val[val.size() -1] == ' ') { + val = val.substr(0, val.size() -1); + } + args.emplace(name, val); + } + } + + auto &config = session.config; + config.monitor.height = util::from_view(args.at("x-nv-video[0].clientViewportHt"sv)); + config.monitor.width = util::from_view(args.at("x-nv-video[0].clientViewportWd"sv)); + config.monitor.framerate = util::from_view(args.at("x-nv-video[0].maxFPS"sv)); + config.monitor.bitrate = util::from_view(args.at("x-nv-video[0].initialBitrateKbps"sv)); + config.monitor.slicesPerFrame = util::from_view(args.at("x-nv-video[0].videoEncoderSlicesPerFrame"sv)); + + config.audio.channels = util::from_view(args.at("x-nv-audio.surround.numChannels"sv)); + config.audio.mask = util::from_view(args.at("x-nv-audio.surround.channelMask"sv)); + config.audio.packetDuration = util::from_view(args.at("x-nv-aqos.packetDuration"sv)); + + config.packetsize = util::from_view(args.at("x-nv-video[0].packetSize"sv)); + + std::copy(std::begin(gcm_key), std::end(gcm_key), std::begin(session.gcm_key)); + std::copy(std::begin(iv), std::end(iv), std::begin(session.iv)); + + session.pingTimeout = std::chrono::steady_clock::now() + config::stream.ping_timeout; + session.client_state = 1; + + session.audioThread = std::thread {audioThread}; + session.videoThread = std::thread {videoThread}; + session.controlThread = std::thread {controlThread}; + + respond(sock, &option, 200, "OK", req->sequenceNumber, {}); +} + +void cmd_play(tcp::socket &&sock, msg_t &&req) { + OPTION_ITEM option {}; + + // I know these string literals will not be modified + option.option = const_cast("CSeq"); + + auto seqn_str = std::to_string(req->sequenceNumber); + option.content = const_cast(seqn_str.c_str()); + + respond(sock, &option, 200, "OK", req->sequenceNumber, {}); +} + +void rtpThread() { + session.client_state = -1; + + asio::io_service io; + + tcp::acceptor acceptor { io, tcp::endpoint { tcp::v6(), RTSP_SETUP_PORT } }; + + std::unordered_map> map_cmd_func; + map_cmd_func.emplace("OPTIONS"sv, &cmd_option); + map_cmd_func.emplace("DESCRIBE"sv, &cmd_describe); + map_cmd_func.emplace("SETUP"sv, &cmd_setup); + map_cmd_func.emplace("ANNOUNCE"sv, &cmd_announce); + + map_cmd_func.emplace("PLAY"sv, &cmd_play); + + while(true) { + tcp::socket sock { io }; + + acceptor.accept(sock); + sock.set_option(tcp::no_delay(true)); + + std::array buf; + + auto len = sock.read_some(asio::buffer(buf)); + buf[std::min(buf.size(), len)] = '\0'; + + msg_t req { new RTSP_MESSAGE {} }; + + parseRtspMessage(req.get(), buf.data(), len); + + print_msg(req.get()); + + auto func = map_cmd_func.find(req->message.request.command); + if(func == std::end(map_cmd_func)) { + cmd_not_found(std::move(sock), std::move(req)); + } + else { + func->second(std::move(sock), std::move(req)); + } + + if(session.client_state == 0) { + session.audioThread.join(); + session.videoThread.join(); + session.controlThread.join(); + + session.client_state = -1; + } + } +} + +} diff --git a/stream.h b/stream.h new file mode 100644 index 00000000..27449a9c --- /dev/null +++ b/stream.h @@ -0,0 +1,19 @@ +// +// Created by loki on 6/5/19. +// + +#ifndef SUNSHINE_STREAM_H +#define SUNSHINE_STREAM_H + +#include "crypto.h" + +namespace stream { + +extern crypto::aes_t gcm_key; +extern crypto::aes_t iv; + +void rtpThread(); + +} + +#endif //SUNSHINE_STREAM_H diff --git a/sunshine.conf b/sunshine.conf new file mode 100644 index 00000000..7b9991fe --- /dev/null +++ b/sunshine.conf @@ -0,0 +1,29 @@ +# Pretty self-explanatory +# If no external IP address is given, the local IP address is used +# external_ip = 123.456.789.12 + +# The private key must be 2048 bits +# pkey = /dir/pkey.pem + +# The certificate must be signed with a 2048 bit key +# cert = /dir/cert.pem + +# Pretty self-explanatory +unique_id = 03904e64-51da-4fb3-9afd-a9f7ff70fea4 + +# The file where info on paired devices is stored +file_devices = devices.xml + +# How long to wait in milliseconds for data from moonlight before shutting down the stream +ping_timeout = 2000 + +############################################### +# FFmpeg software encoding parameters +# Honestly, I have no idea what the optimal values would be. +# Play around with this :) +max_b_frames = 16 +gop_size = 24 + +# Constant Rate Factor. Between 1 and 52. +# Higher value means more compression, but less quality +crf = 35 diff --git a/utility.h b/utility.h new file mode 100644 index 00000000..9521f8e3 --- /dev/null +++ b/utility.h @@ -0,0 +1,643 @@ +#ifndef UTILITY_H +#define UTILITY_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define KITTY_DEFAULT_CONSTR(x)\ + x(x&&) noexcept = default;\ + x&operator=(x&&) noexcept = default;\ + x() = default; + +#define KITTY_DEFAULT_CONSTR_THROW(x)\ + x(x&&) = default;\ + x&operator=(x&&) = default;\ + x() = default; + +#define TUPLE_2D(a,b, expr)\ + decltype(expr) a##_##b = expr;\ + auto &a = std::get<0>(a##_##b);\ + auto &b = std::get<1>(a##_##b) + +#define TUPLE_2D_REF(a,b, expr)\ + auto &a##_##b = expr;\ + auto &a = std::get<0>(a##_##b);\ + auto &b = std::get<1>(a##_##b) + +#define TUPLE_3D(a,b,c, expr)\ + decltype(expr) a##_##b##_##c = expr;\ + auto &a = std::get<0>(a##_##b##_##c);\ + auto &b = std::get<1>(a##_##b##_##c);\ + auto &c = std::get<2>(a##_##b##_##c) + +#define TUPLE_3D_REF(a,b,c, expr)\ + auto &a##_##b##_##c = expr;\ + auto &a = std::get<0>(a##_##b##_##c);\ + auto &b = std::get<1>(a##_##b##_##c);\ + auto &c = std::get<2>(a##_##b##_##c) + +namespace util { + +template class X, class...Y> +struct __instantiation_of : public std::false_type {}; + +template class X, class... Y> +struct __instantiation_of> : public std::true_type {}; + +template class X, class T, class...Y> +static constexpr auto instantiation_of_v = __instantiation_of::value; + +template +struct __either; + +template +struct __either { + using type = X; +}; + +template +struct __either { + using type = Y; +}; + +template +using either_t = typename __either::type; + +template +struct __false_v; + +template +struct __false_v>> { + static constexpr std::nullopt_t value = std::nullopt; +}; + +template +struct __false_v || instantiation_of_v || instantiation_of_v) + >> { + static constexpr std::nullptr_t value = nullptr; +}; + +template +struct __false_v>> { + static constexpr bool value = false; +}; + +template +static constexpr auto false_v = __false_v::value; + +template +class FailGuard { +public: + FailGuard() = delete; + FailGuard(T && f) noexcept : _func { std::forward(f) } {} + FailGuard(FailGuard &&other) noexcept : _func { std::move(other._func) } { + this->failure = other.failure; + + other.failure = false; + } + + FailGuard(const FailGuard &) = delete; + + FailGuard &operator=(const FailGuard &) = delete; + FailGuard &operator=(FailGuard &&other) = delete; + + ~FailGuard() noexcept { + if(failure) { + _func(); + } + } + + void disable() { failure = false; } + bool failure { true }; +private: + T _func; +}; + +template +auto fail_guard(T && f) { + return FailGuard { std::forward(f) }; +} + +template +void append_struct(std::vector &buf, const T &_struct) { + constexpr size_t data_len = sizeof(_struct); + + buf.reserve(data_len); + + auto *data = (uint8_t *) & _struct; + + for (size_t x = 0; x < data_len; ++x) { + buf.push_back(data[x]); + } +} + +template +class Hex { +public: + typedef T elem_type; +private: + const char _bits[16] { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + + char _hex[sizeof(elem_type) * 2]; +public: + Hex(const elem_type &elem, bool rev) { + if(!rev) { + const uint8_t *data = reinterpret_cast(&elem) + sizeof(elem_type) - 1; + for (auto it = begin(); it < cend();) { + *it++ = _bits[*data / 16]; + *it++ = _bits[*data-- % 16]; + } + } + else { + const uint8_t *data = reinterpret_cast(&elem); + for (auto it = begin(); it < cend();) { + *it++ = _bits[*data / 16]; + *it++ = _bits[*data++ % 16]; + } + } + } + + char *begin() { return _hex; } + char *end() { return _hex + sizeof(elem_type) * 2; } + + const char *begin() const { return _hex; } + const char *end() const { return _hex + sizeof(elem_type) * 2; } + + const char *cbegin() const { return _hex; } + const char *cend() const { return _hex + sizeof(elem_type) * 2; } + + std::string to_string() const { + return { begin(), end() }; + } + + std::string_view to_string_view() const { + return { begin(), sizeof(elem_type) * 2 }; + } +}; + +template +Hex hex(const T &elem, bool rev = false) { + return Hex(elem, rev); +} + +template +std::string hex_vec(It begin, It end, bool rev = false) { + auto str_size = 2*std::distance(begin, end); + + + std::string hex; + hex.resize(str_size); + + const char _bits[16] { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + + if(rev) { + for (auto it = std::begin(hex); it < std::end(hex);) { + *it++ = _bits[((uint8_t)*begin) / 16]; + *it++ = _bits[((uint8_t)*begin++) % 16]; + } + } + else { + --end; + for (auto it = std::begin(hex); it < std::end(hex);) { + *it++ = _bits[((uint8_t)*end) / 16]; + *it++ = _bits[((uint8_t)*end--) % 16]; + } + } + + + return hex; +} + +template +std::string hex_vec(C&& c, bool rev = false) { + return hex_vec(std::begin(c), std::end(c), rev); +} + +template +std::optional from_hex(const std::string_view &hex, bool rev = false) { + std::uint8_t buf[sizeof(T)]; + + static char constexpr shift_bit = 'a' - 'A'; + auto is_convertable = [] (char ch) -> bool { + if(isdigit(ch)) { + return true; + } + + ch |= shift_bit; + + if('a' > ch || ch > 'z') { + return false; + } + + return true; + }; + + auto buf_size = std::count_if(std::begin(hex), std::end(hex), is_convertable) / 2; + if(buf_size != sizeof(T)) { + return std::nullopt; + } + + const char *data = hex.data() + hex.size() -1; + + auto convert = [] (char ch) -> std::uint8_t { + if(ch >= '0' && ch <= '9') { + return (std::uint8_t)ch - '0'; + } + + return (std::uint8_t)(ch | (char)32) - 'a' + (char)10; + }; + + for(auto &el : buf) { + while(!is_convertable(*data)) { --data; } + std::uint8_t ch_r = convert(*data--); + + while(!is_convertable(*data)) { --data; } + std::uint8_t ch_l = convert(*data--); + + el = (ch_l << 4) | ch_r; + } + + if(rev) { + std::reverse(std::begin(buf), std::end(buf)); + } + + return *reinterpret_cast(buf); +} + +inline std::string from_hex_vec(const std::string &hex, bool rev = false) { + std::string buf; + + static char constexpr shift_bit = 'a' - 'A'; + auto is_convertable = [] (char ch) -> bool { + if(isdigit(ch)) { + return true; + } + + ch |= shift_bit; + + if('a' > ch || ch > 'z') { + return false; + } + + return true; + }; + + auto buf_size = std::count_if(std::begin(hex), std::end(hex), is_convertable) / 2; + buf.resize(buf_size); + + const char *data = hex.data() + hex.size() -1; + + auto convert = [] (char ch) -> std::uint8_t { + if(ch >= '0' && ch <= '9') { + return (std::uint8_t)ch - '0'; + } + + return (std::uint8_t)(ch | (char)32) - 'a' + (char)10; + }; + + for(auto &el : buf) { + while(!is_convertable(*data)) { --data; } + std::uint8_t ch_r = convert(*data--); + + while(!is_convertable(*data)) { --data; } + std::uint8_t ch_l = convert(*data--); + + el = (ch_l << 4) | ch_r; + } + + if(rev) { + std::reverse(std::begin(buf), std::end(buf)); + } + + return buf; +} + +template +class hash { +public: + using value_type = T; + std::size_t operator()(const value_type &value) const { + const auto *p = reinterpret_cast(&value); + + return std::hash{}(std::string_view { p, sizeof(value_type) }); + } +}; + +template +auto enm(const T& val) -> const std::underlying_type_t& { + return *reinterpret_cast*>(&val); +} + +template +auto enm(T& val) -> std::underlying_type_t& { + return *reinterpret_cast*>(&val); +} + +template +struct Function { + typedef ReturnType (*type)(Args...); +}; + +template::type function> +struct Destroy { + typedef T pointer; + + void operator()(pointer p) { + function(p); + } +}; + +template::type function> +using safe_ptr = std::unique_ptr>; + +// You cannot specialize an alias +template::type function> +using safe_ptr_v2 = std::unique_ptr>; + +template +void c_free(T *p) { + free(p); +} + +template +using c_ptr = safe_ptr>; + +template +class FakeContainer { + typedef T pointer; + + pointer _begin; + pointer _end; + +public: + FakeContainer(pointer begin, pointer end) : _begin(begin), _end(end) {} + + pointer begin() { return _begin; } + pointer end() { return _end; } + + const pointer begin() const { return _begin; } + const pointer end() const { return _end; } + + const pointer cbegin() const { return _begin; } + const pointer cend() const { return _end; } + + pointer data() { return begin(); } + const pointer data() const { return cbegin(); } + + std::size_t size() const { return std::distance(begin(), end()); } +}; + +template +FakeContainer toContainer(T begin, T end) { + return { begin, end }; +} + +template +FakeContainer toContainer(T begin, std::size_t end) { + return { begin, begin + end }; +} + +template +FakeContainer toContainer(T * const begin) { + T *end = begin; + + auto default_val = T(); + while(*end != default_val) { + ++end; + } + + return toContainer(begin, end); +} + +template +struct _init_helper; + +template class T, class H, class... Args> +struct _init_helper, H> { + using type = T; + + static type move(Args&&... args, H&&) { + return std::make_tuple(std::move(args)...); + } + + static type copy(const Args&... args, const H&) { + return std::make_tuple(args...); + } +}; + +inline std::int64_t from_chars(const char *begin, const char *end) { + std::int64_t res {}; + std::int64_t mul = 1; + while(begin != --end) { + res += (std::int64_t)(*end - '0') * mul; + + mul *= 10; + } + + return *begin != '-' ? res + (std::int64_t)(*begin - '0') * mul : -res; +} + +inline std::int64_t from_view(const std::string_view &number) { + return from_chars(std::begin(number), std::end(number)); +} + +template +class Either : public std::variant { +public: + using std::variant::variant; + + constexpr bool has_left() const { + return std::holds_alternative(*this); + } + constexpr bool has_right() const { + return std::holds_alternative(*this); + } + + X &left() { + return std::get(*this); + } + + Y &right() { + return std::get(*this); + } + + const X &left() const { + return std::get(*this); + } + + const Y &right() const { + return std::get(*this); + } +}; + +template +class buffer_t { +public: + buffer_t() : _els { 0 } {}; + buffer_t(buffer_t&&) noexcept = default; + buffer_t &operator=(buffer_t&& other) noexcept { + std::swap(_els, other._els); + + _buf = std::move(other._buf); + + return *this; + }; + + explicit buffer_t(size_t elements) : _els { elements }, _buf { std::make_unique(elements) } {} + explicit buffer_t(size_t elements, const T &t) : _els { elements }, _buf { std::make_unique(elements) } { + std::fill_n(_buf.get(), elements, t); + } + + T &operator[](size_t el) { + return _buf[el]; + } + + const T &operator[](size_t el) const { + return _buf[el]; + } + + size_t size() const { + return _els; + } + + void fake_resize(std::size_t els) { + _els = els; + } + + T *begin() { + return _buf.get(); + } + + const T *begin() const { + return _buf.get(); + } + + T *end() { + return _buf.get() + _els; + } + + const T *end() const { + return _buf.get() + _els; + } + +private: + size_t _els; + std::unique_ptr _buf; +}; + + +template +T either(std::optional &&l, T &&r) { + if(l) { + return std::move(*l); + } + + return std::forward(r); +} + +namespace endian { +template +struct endianness { + enum : bool { +#if defined(__BYTE_ORDER) && __BYTE_ORDER == __BIG_ENDIAN || \ + defined(__BIG_ENDIAN__) || \ + defined(__ARMEB__) || \ + defined(__THUMBEB__) || \ + defined(__AARCH64EB__) || \ + defined(_MIBSEB) || defined(__MIBSEB) || defined(__MIBSEB__) + // It's a big-endian target architecture + little = false, +#elif defined(__BYTE_ORDER) && __BYTE_ORDER == __LITTLE_ENDIAN || \ + defined(__LITTLE_ENDIAN__) || \ + defined(__ARMEL__) || \ + defined(__THUMBEL__) || \ + defined(__AARCH64EL__) || \ + defined(_MIPSEL) || defined(__MIPSEL) || defined(__MIPSEL__) + // It's a little-endian target architecture + little = true, +#else +#error "Unknown Endianness" +#endif + big = !little + }; +}; + +template +struct endian_helper { }; + +template +struct endian_helper) +>> { + static inline T big(T x) { + if constexpr (endianness::little) { + uint8_t *data = reinterpret_cast(&x); + + std::reverse(data, data + sizeof(x)); + } + + return x; + } + + static inline T little(T x) { + if constexpr (endianness::big) { + uint8_t *data = reinterpret_cast(&x); + + std::reverse(data, data + sizeof(x)); + } + + return x; + } +}; + +template +struct endian_helper +>> { +static inline T little(T x) { + if(!x) return x; + + if constexpr (endianness::big) { + auto *data = reinterpret_cast(&*x); + + std::reverse(data, data + sizeof(*x)); + } + + return x; +} + + +static inline T big(T x) { + if(!x) return x; + + if constexpr (endianness::big) { + auto *data = reinterpret_cast(&*x); + + std::reverse(data, data + sizeof(*x)); + } + + return x; +} +}; + +template +inline auto little(T x) { return endian_helper::little(x); } + +template +inline auto big(T x) { return endian_helper::big(x); } +} /* endian */ + +} /* util */ +#endif diff --git a/uuid.h b/uuid.h new file mode 100644 index 00000000..19b8fa1f --- /dev/null +++ b/uuid.h @@ -0,0 +1,50 @@ +// +// Created by loki on 8-2-19. +// + +#ifndef T_MAN_UUID_H +#define T_MAN_UUID_H + +#include + +union uuid_t { + std::uint8_t b8[16]; + std::uint16_t b16[8]; + std::uint32_t b32[4]; + std::uint64_t b64[2]; + + static uuid_t generate(std::default_random_engine &engine) { + std::uniform_int_distribution dist(0, std::numeric_limits::max()); + + uuid_t buf; + for(auto &el : buf.b8) { + el = dist(engine); + } + + buf.b8[7] &= (std::uint8_t) 0b00101111; + buf.b8[9] &= (std::uint8_t) 0b10011111; + + return buf; + } + + static uuid_t generate() { + std::random_device r; + + std::default_random_engine engine { r() }; + + return generate(engine); + } + + constexpr bool operator==(const uuid_t &other) const { + return b64[0] == other.b64[0] && b64[1] == other.b64[1]; + } + + constexpr bool operator<(const uuid_t &other) const { + return (b64[0] < other.b64[0] || (b64[0] == other.b64[0] && b64[1] < other.b64[1])); + } + + constexpr bool operator>(const uuid_t &other) const { + return (b64[0] > other.b64[0] || (b64[0] == other.b64[0] && b64[1] > other.b64[1])); + } +}; +#endif //T_MAN_UUID_H diff --git a/video.cpp b/video.cpp new file mode 100644 index 00000000..51706995 --- /dev/null +++ b/video.cpp @@ -0,0 +1,176 @@ +// +// Created by loki on 6/6/19. +// + +#include +#include +#include + +#include + +extern "C" { +#include +#include +} + +#include "config.h" +#include "video.h" + +namespace video { +using namespace std::literals; + +void free_ctx(AVCodecContext *ctx) { + avcodec_free_context(&ctx); +} + +void free_frame(AVFrame *frame) { + av_frame_free(&frame); +} + +void free_packet(AVPacket *packet) { + av_packet_free(&packet); +} + +using ctx_t = util::safe_ptr; +using frame_t = util::safe_ptr; + +using sws_t = util::safe_ptr; + +auto open_codec(ctx_t &ctx, AVCodec *codec, AVDictionary **options) { + avcodec_open2(ctx.get(), codec, options); + + return util::fail_guard([&]() { + avcodec_close(ctx.get()); + }); +} + +void encode(int64_t frame, ctx_t &ctx, sws_t &sws, frame_t &yuv_frame, platf::img_t &img, std::shared_ptr> &packets) { + av_frame_make_writable(yuv_frame.get()); + + const int linesizes[2] { + (int)(platf::img_width(img) * sizeof(int)), 0 + }; + + auto data = platf::img_data(img); + int ret = sws_scale(sws.get(), (uint8_t*const*)&data, linesizes, 0, platf::img_height(img), yuv_frame->data, yuv_frame->linesize); + + if(ret <= 0) { + exit(1); + } + + yuv_frame->pts = frame; + + /* send the frame to the encoder */ + ret = avcodec_send_frame(ctx.get(), yuv_frame.get()); + if (ret < 0) { + fprintf(stderr, "error sending a frame for encoding\n"); + exit(1); + } + + while (ret >= 0) { + packet_t packet { av_packet_alloc() }; + + ret = avcodec_receive_packet(ctx.get(), packet.get()); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) + return; + else if (ret < 0) { + fprintf(stderr, "error during encoding\n"); + exit(1); + } + + packets->push(std::move(packet)); + } +} + +void encodeThread( + std::shared_ptr> images, + std::shared_ptr> packets, config_t config) { + int framerate = config.framerate; + + auto codec = avcodec_find_encoder(AV_CODEC_ID_H264); + + ctx_t ctx{avcodec_alloc_context3(codec)}; + + frame_t yuv_frame{av_frame_alloc()}; + + ctx->width = config.width; + ctx->height = config.height; + ctx->bit_rate = config.bitrate; + ctx->time_base = AVRational{1, framerate}; + ctx->framerate = AVRational{framerate, 1}; + ctx->pix_fmt = AV_PIX_FMT_YUV420P; + ctx->max_b_frames = config::video.max_b_frames; + ctx->gop_size = config::video.gop_size; + + ctx->slices = config.slicesPerFrame; + ctx->thread_type = FF_THREAD_SLICE; + ctx->thread_count = std::min(config.slicesPerFrame, 4); + + AVDictionary *options {nullptr}; + av_dict_set(&options, "preset", "ultrafast", 0); + // av_dict_set(&options, "tune", "fastdecode", 0); + av_dict_set(&options, "profile", "baseline", 0); + + av_dict_set_int(&options, "crf", config::video.crf, 0); + + ctx->flags |= (AV_CODEC_FLAG_CLOSED_GOP | AV_CODEC_FLAG_LOW_DELAY); + ctx->flags2 |= AV_CODEC_FLAG2_FAST; + + auto fromformat = AV_PIX_FMT_BGR0; + auto lg = open_codec(ctx, codec, &options); + + yuv_frame->format = ctx->pix_fmt; + yuv_frame->width = ctx->width; + yuv_frame->height = ctx->height; + + av_frame_get_buffer(yuv_frame.get(), 0); + + int64_t frame = 1; + + // Initiate scaling context with correct height and width + sws_t sws; + if(auto img = images->pop()) { + sws.reset( + sws_getContext( + platf::img_width(img), platf::img_height(img), fromformat, + ctx->width, ctx->height, ctx->pix_fmt, + SWS_LANCZOS | SWS_ACCURATE_RND, + nullptr, nullptr, nullptr)); + } + + while (auto img = images->pop()) { + encode(frame++, ctx, sws, yuv_frame, img, packets); + } + + packets->stop(); +} + +void capture_display(std::shared_ptr> packets, config_t config) { + int framerate = config.framerate; + + std::shared_ptr> images { new safe::queue_t }; + + std::thread encoderThread { &encodeThread, images, packets, config }; + + auto disp = platf::display(); + + auto time_span = std::chrono::floor(1s) / framerate; + while(packets->running()) { + auto next_snapshot = std::chrono::steady_clock::now() + time_span; + auto img = platf::snapshot(disp); + + images->push(std::move(img)); + img.reset(); + + auto t = std::chrono::steady_clock::now(); + if(t > next_snapshot) { + std::cout << "Taking snapshot took "sv << std::chrono::floor(t - next_snapshot).count() << " milliseconds too long"sv << std::endl; + } + + std::this_thread::sleep_until(next_snapshot); + } + + images->stop(); + encoderThread.join(); +} +} diff --git a/video.h b/video.h new file mode 100644 index 00000000..1c392003 --- /dev/null +++ b/video.h @@ -0,0 +1,27 @@ +// +// Created by loki on 6/9/19. +// + +#ifndef SUNSHINE_VIDEO_H +#define SUNSHINE_VIDEO_H + +#include "queue.h" + +struct AVPacket; +namespace video { +void free_packet(AVPacket *packet); + +using packet_t = util::safe_ptr; + +struct config_t { + int width; + int height; + int framerate; + int bitrate; + int slicesPerFrame; +}; + +void capture_display(std::shared_ptr> packets, config_t config); +} + +#endif //SUNSHINE_VIDEO_H