commit ae29230f599f529ef4b1e14e204d3b47b4494b3b Author: loki Date: Tue Dec 3 20:23:33 2019 +0100 Removed Git history due to personal info 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 00000000..b3f6c0ad Binary files /dev/null and b/assets/box.png differ diff --git a/assets/demoCA/cacert.pem b/assets/demoCA/cacert.pem new file mode 100644 index 00000000..c4b79cc9 --- /dev/null +++ b/assets/demoCA/cacert.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDsTCCApmgAwIBAgIULnRRHDUzdg4a9dwWi0/yV5LADLIwDQYJKoZIhvcNAQEL +BQAwaDELMAkGA1UEBhMCTkwxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEhMB8GCSqGSIb3DQEJARYSbG9raUBm +YWtlZW1haWwuY29tMB4XDTE5MDYwMzEwMzY0N1oXDTI5MDUzMTEwMzY0N1owaDEL +MAkGA1UEBhMCTkwxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVy +bmV0IFdpZGdpdHMgUHR5IEx0ZDEhMB8GCSqGSIb3DQEJARYSbG9raUBmYWtlZW1h +aWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4cyNQ7uH6tAE +EAD99oxR1CVYlOgRDEUqJCjkpIoVF5uE3/kfuvEJ+l7/WL51lqbq35uL5ta7EbJH +BvVyNH00FN2D/Yl0n/X+TlhRP88MS5F0d0rwyQEAh4wEGcUhdSoW9TfybmLaHeCC +bZlxC3kBWxr0e6YDlV9deM6j+OjCerQwkiiGwgE9dVrVCj1dLyzBhWnYGpYBvY+3 +6kEy0Vmf2spGCB6meCAMrAMz75fUDuk8YRF0umb+SLA44AB/U6d6GXU2EjpTuPww +OMkUr8EmdbAI3l1tmWJTAkhFQ7681AyIWYOspc1biXZdBrvNBTV8kbDGlomNj19V +QhhN44d4ywIDAQABo1MwUTAdBgNVHQ4EFgQUFRktN33zyW4MR9Cy1Vcn+B+EdYAw +HwYDVR0jBBgwFoAUFRktN33zyW4MR9Cy1Vcn+B+EdYAwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAQEAkpU+ALElNz+5jOnVAPyYXYsJKfevmKK9uK4v +V+l7GrFLIEhC2qr26S8Nd2pLkrgesCD4xfoOONiVOceU5igh9acFA3+NyOSFLdRN +bSdy0jvCuoiK46ieDAagQtdt0G7HGV4u+jWz0jaKUQI9zJqznOHdJV6RZFIqTYLG +KHnGP+mtXjW3E1djU9vFreYcB6UY+Ai1KB33dnBK9Es2fIQhikKZUPTh6BYsRZT6 +U7c6fh+01fRhRPo/SCFmY993857NtoOHMeP0M2V65CG4VjpAPR0msChVQVv7csca +TvBvB23dFRTLbo5PUSWC9bhBrMjzJ7yylt1CNBBHv7ycw8yIPw== +-----END CERTIFICATE----- diff --git a/assets/demoCA/cakey.pem b/assets/demoCA/cakey.pem new file mode 100644 index 00000000..b793eb26 --- /dev/null +++ b/assets/demoCA/cakey.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDhzI1Du4fq0AQQ +AP32jFHUJViU6BEMRSokKOSkihUXm4Tf+R+68Qn6Xv9YvnWWpurfm4vm1rsRskcG +9XI0fTQU3YP9iXSf9f5OWFE/zwxLkXR3SvDJAQCHjAQZxSF1Khb1N/JuYtod4IJt +mXELeQFbGvR7pgOVX114zqP46MJ6tDCSKIbCAT11WtUKPV0vLMGFadgalgG9j7fq +QTLRWZ/aykYIHqZ4IAysAzPvl9QO6TxhEXS6Zv5IsDjgAH9Tp3oZdTYSOlO4/DA4 +yRSvwSZ1sAjeXW2ZYlMCSEVDvrzUDIhZg6ylzVuJdl0Gu80FNXyRsMaWiY2PX1VC +GE3jh3jLAgMBAAECggEAdnHgoGkc8RXRK7v5fH0653f3sZTSbIdThchVt+IfElUo +LHz4Ig4S191BQQIXmMFSb52ek6aMVsoX7BSQpewPh+pzNGoIXWiiz+IQLNKldnaE +i5cqG6aE6pWOCR6ZYGaFyHhimXkNRaLhiDB3VjdReML5AGujcZWm6Jos9YLTkZ07 +pYXs2S+/5oNbfDdAE8dgdD7vD9lNGrbtmJ9+J+VvgPOYM/4LaEAfZU0ALDm2Hl3n +CqkZCp+eWbQK3MC2+6Y4yS+jo7e5/nKX1De9StkX6KumAEIpxkHPZF2EnWXW64oD +k40tKXT+oMXF1RLb4scnv+J+uR4bdl+Xq/VTzKrtsQKBgQDyoOjrv+g5l0s0KKDJ +qPkLQNRCJpzU7Km3CjPuk8DOlmBCr4KUeVfjbYPxQQMkeNL9zFz/QYBmabTCE6Ih +E8BbzT/RQzVPSOOh//Hh4eN4umrYaOIkY4Bv2R2X3H0ELFq+G7pKdvTAkusGHcWu +OAUb2HO5rTiRYq8DvHy8b+qH7wKBgQDuPjR+DCVtkBXmBNf0FZkl7OW/EUQBq4+T +WoaYleq8Gd7ubJgz2Gud+0+L41VjFD9W3hkJAb/3wkASseQ+2hk3Sv/BzpAIdS9e +tN/xxp/8NK7tJGL63m6tAfX+Hi/kSDEp45Zp3PoOD08MEK0yIrpf7S8uJ++u49K4 +tKXkGCIg5QKBgFujvPW2AQ8nfqcPpVMleBLxBHqLvPaLALr6poy4z7z3fRoS0j4j +6rcimRAZHwe6fu6PLpzWb5m+2R/obHcTz7acujreqJbuj9OTKRfIyrLBrjNYwfk+ +f7c/CPdftvRJkGh3bpBLh7vogc5Ilm5sCDnxMhxyOYhn/nRpz68YkjuPAoGBAOQX +6DfZtyfLcDvV3U/SMdsOkPO6OwsCTya73+tMdP18I2TP0XSpunb5ebIrh7+hTfcE +EqH96+XwM1nyuNy4ALZgdrb95gZC84RP1axsBxX29pcSZDVdKkc3fmW6Tw3XVEKP +o51dNIarf3nEqZ07hIZ81dPx5lbhxgiS49SaimpFAoGBAKamKZFAfUHlaHV/Na1C +3SZji7PaDSj1EFmRkCySK9VqD7Tbh1abrpC2ImdhYHn5TcQQE2eidB+F0Nf6IhKN +upBTofg0ebaslo+BYAqAsRKnUQGDToGIIIdXJ6DnO3wxWu9GY4nKdl3jxqAv2A2x +d8SETw4wqlFFRO33opycuFS5 +-----END PRIVATE KEY----- diff --git a/audio.cpp b/audio.cpp new file mode 100644 index 00000000..b7bb2df7 --- /dev/null +++ b/audio.cpp @@ -0,0 +1,104 @@ +#include +#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