diff --git a/CMakeLists.txt b/CMakeLists.txt index 0beb0b7..10f9392 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,8 +4,8 @@ SET(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") include(${CMAKE_ROOT}/Modules/GNUInstallDirs.cmake) set(MOONLIGHT_MAJOR_VERSION 2) -set(MOONLIGHT_MINOR_VERSION 1) -set(MOONLIGHT_PATCH_VERSION 4) +set(MOONLIGHT_MINOR_VERSION 2) +set(MOONLIGHT_PATCH_VERSION 0) set(MOONLIGHT_VERSION ${MOONLIGHT_MAJOR_VERSION}.${MOONLIGHT_MINOR_VERSION}.${MOONLIGHT_PATCH_VERSION}) aux_source_directory(./src SRC_LIST) @@ -17,6 +17,7 @@ find_package(ALSA REQUIRED) find_package(Opus REQUIRED) find_package(Broadcom) find_package(Freescale) +find_package(Amlogic) find_package(PkgConfig REQUIRED) pkg_check_modules(EVDEV REQUIRED libevdev) @@ -40,14 +41,14 @@ else() set(SOFTWARE_FOUND FALSE) endif() -SET(MOONLIGHT_COMMON_INCLUDE_DIR ./third_party/moonlight-common-c) +SET(MOONLIGHT_COMMON_INCLUDE_DIR ./third_party/moonlight-common-c/src) SET(GAMESTREAM_INCLUDE_DIR ./libgamestream) if(CMAKE_BUILD_TYPE MATCHES Debug) list(APPEND SRC_LIST ./src/video/fake.c) list(APPEND MOONLIGHT_DEFINITIONS HAVE_FAKE LC_DEBUG) list(APPEND MOONLIGHT_OPTIONS FAKE DEBUG) -elseif(NOT BROADCOM_FOUND AND NOT FREESCALE_FOUND AND NOT SOFTWARE_FOUND) +elseif(NOT AMLOGIC_FOUND AND NOT BROADCOM_FOUND AND NOT FREESCALE_FOUND AND NOT SOFTWARE_FOUND) message(FATAL_ERROR "No video output available") endif() @@ -62,7 +63,7 @@ if (SOFTWARE_FOUND) endif() endif() -if (BROADCOM_FOUND OR FREESCALE_FOUND OR CMAKE_BUILD_TYPE MATCHES Debug) +if (AMLOGIC_FOUND OR BROADCOM_FOUND OR FREESCALE_FOUND OR CMAKE_BUILD_TYPE MATCHES Debug) list(APPEND MOONLIGHT_DEFINITIONS HAVE_EMBEDDED) list(APPEND MOONLIGHT_OPTIONS EMBEDDED) endif() @@ -90,13 +91,23 @@ if (CEC_FOUND) target_link_libraries(moonlight ${CEC_LIBRARIES}) endif() +if(AMLOGIC_FOUND) + list(APPEND MOONLIGHT_DEFINITIONS HAVE_AML) + list(APPEND MOONLIGHT_OPTIONS AML) + add_library(moonlight-aml SHARED ./src/video/aml.c ${ILCLIENT_SRC_LIST}) + target_include_directories(moonlight-aml PRIVATE ${AMLOGIC_INCLUDE_DIRS} ${GAMESTREAM_INCLUDE_DIR} ${MOONLIGHT_COMMON_INCLUDE_DIR}) + target_link_libraries(moonlight-aml gamestream ${AMLOGIC_LIBRARIES}) + set_property(TARGET moonlight-aml PROPERTY COMPILE_DEFINITIONS ${AMLOGIC_DEFINITIONS}) + install(TARGETS moonlight-aml DESTINATION ${CMAKE_INSTALL_LIBDIR}) +endif() + if(BROADCOM_FOUND) list(APPEND MOONLIGHT_DEFINITIONS HAVE_PI) list(APPEND MOONLIGHT_OPTIONS PI) aux_source_directory(./third_party/ilclient ILCLIENT_SRC_LIST) - add_library(moonlight-pi SHARED ./src/video/pi.c ${ILCLIENT_SRC_LIST}) - target_include_directories(moonlight-pi PRIVATE ./third_party/ilclient ${BROADCOM_INCLUDE_DIRS} ${GAMESTREAM_INCLUDE_DIR} ${MOONLIGHT_COMMON_INCLUDE_DIR}) - target_link_libraries(moonlight-pi gamestream ${BROADCOM_LIBRARIES}) + add_library(moonlight-pi SHARED ./src/video/pi.c ./src/audio/omx.c ${ILCLIENT_SRC_LIST}) + target_include_directories(moonlight-pi PRIVATE ./third_party/ilclient ${BROADCOM_INCLUDE_DIRS} ${GAMESTREAM_INCLUDE_DIR} ${MOONLIGHT_COMMON_INCLUDE_DIR} ${OPUS_INCLUDE_DIRS}) + target_link_libraries(moonlight-pi gamestream ${BROADCOM_LIBRARIES} ${OPUS_LIBRARY}) set_property(TARGET moonlight-pi PROPERTY COMPILE_DEFINITIONS ${BROADCOM_DEFINITIONS}) install(TARGETS moonlight-pi DESTINATION ${CMAKE_INSTALL_LIBDIR}) endif() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1d610d3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# Contribution Guide + +## Got a Question or Problem? +Please take a look at the [wiki](https://github.com/irtimmer/moonlight-embedded/wiki) to see if answers your questions about the usage of Moonlight Embedded. + +If you still have questions about Moonlight Embedded, please use one of the different fora discussing Moonlight Embedded. + +[XDA](http://forum.xda-developers.com/showthread.php?t=2505510) Moonlight in General +[Raspberry Pi Forum](http://www.raspberrypi.org/forums/viewtopic.php?f=78&t=65878) Moonlight Embedded for Raspberry Pi +[SolidRun Community](http://www.solid-run.com/community/viewtopic.php?f=13&t=1489&p=11173) Moonlight Embedded for Cubox-i and Hummingboard +[ODROID Forum](http://forum.odroid.com/viewtopic.php?f=91&t=15456) Moonlight Embedded on ODROID + +## Found an Issue? +If you think you found a bug in Moonlight Embedded you can submit a issue. But please ensure first you have checked the following or otherwise we will mark the issue as invalid: +- [ ] It's a bug in Moonlight Embedded and not in NVidia Geforce Experience or Steam as we otherwise can't fix them +- [ ] It's not a misconfiguration of your own setup. Like firewall misconfiguration. +- [ ] Their is no other bug report with the same issue. Check also the closed issues in case your bug is already solved in master. + +Also provide as much information as possible about your setup and how to produce the issue so their are higher chances we can reproduce the issue and fix it. Even better you can submit a Pull Request +with a fix. + +## Feature request +There are not much developers working on Moonlight Embedded. So it currently doesn't make much sense to use the issue tracker to submit feature request. Please try to implement it yourself and submit a pull request or discuss it on one of the fora to see if someone else is able to implement it. + +## Submitting a Pull Request +Have you created a cool new feature or fixed a few bugs you can submit a pull request. But before your request is merged you have to check the following. +- [ ] Your branch is based on a recent commit and can be merge cleanly +- [ ] Your code uses the same code style as the rest of the code +- [ ] Your history is cleanup and you provide one or multiple commits +- [ ] Your commits only changes the necessery lines and not accidently changes whitespace or add or remove empty lines. + +If these guide lines are not met we maybe won't merge your pull request or take some to cleanup your pull request before merging. Depending on how bad we wan't your code. \ No newline at end of file diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..4dab664 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,11 @@ +Please provide the following info. + +**_NVidia Geforce Experience version:_** +**Moonlight Embedded version:** +**Moonlight Embedded running on:** _Raspberry Pi/Cubox-i/Hummingboard/Other linux device/..._ + +**Output of Moonlight Embedded:** + +**What is the expected result?** + +**What happens instead of that?** diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..6a0bebe --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,3 @@ +**Description** + +**Purpose** diff --git a/README.md b/README.md index 6ded03c..cdc922b 100644 --- a/README.md +++ b/README.md @@ -3,19 +3,12 @@ Moonlight Embedded is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built for Linux. Moonlight Embedded allows you to stream your full collection of Steam games from -your powerful Windows desktop to your (embedded) Linux system, like Raspberry Pi, CuBox-i and Hummingboard. +your powerful Windows desktop to your (embedded) Linux system, like Raspberry Pi, CuBox-i and ODROID. ## Documentation More information about installing and runnning Moonlight Embedded is available on the [wiki](https://github.com/irtimmer/moonlight-embedded/wiki). -## Features - -* Streams Steam and all of your games from your PC to your embedded system. -* Use mDNS to scan for compatible GeForce Experience (GFE) machines on the network. -* Qwerty Keyboard, Mouse and Gamepad support -* Support hardware video decoding on Raspberry Pi and i.MX 6 devices - ## Requirements * [GFE compatible](http://shield.nvidia.com/play-pc-games/) computer with GTX 600/700/900 series GPU (for the PC you're streaming from) @@ -33,6 +26,8 @@ More information about installing and runnning Moonlight Embedded is available o ## Bugs +Please check the fora, wiki and old bug reports before submitting a new bug report. + Bugs can be reported to the [issue tracker](https://github.com/irtimmer/moonlight-embedded/issues). ## See also @@ -44,9 +39,10 @@ different C implementations of Moonlight ## Discussion -[XDA](http://forum.xda-developers.com/showthread.php?t=2505510) -[Raspberry Pi Forum](http://www.raspberrypi.org/forums/viewtopic.php?f=78&t=65878) -[SolidRun Community](http://www.solid-run.com/community/viewtopic.php?f=13&t=1489&p=11173) +[XDA](http://forum.xda-developers.com/showthread.php?t=2505510) Moonlight in General +[Raspberry Pi Forum](http://www.raspberrypi.org/forums/viewtopic.php?f=78&t=65878) Moonlight Embedded for Raspberry Pi +[SolidRun Community](http://www.solid-run.com/community/viewtopic.php?f=13&t=1489&p=11173) Moonlight Embedded for Cubox-i and Hummingboard +[ODROID Forum](http://forum.odroid.com/viewtopic.php?f=91&t=15456) Moonlight Embedded on ODROID ## Contribute diff --git a/cmake/FindAmlogic.cmake b/cmake/FindAmlogic.cmake new file mode 100644 index 0000000..eb06031 --- /dev/null +++ b/cmake/FindAmlogic.cmake @@ -0,0 +1,29 @@ +find_path(AMLOGIC_INCLUDE_DIR + NAMES codec.h + DOC "Amlogic include directory" + PATHS /usr/local/include/amcodec /usr/include/amcodec) +mark_as_advanced(AMLOGIC_INCLUDE_DIR) + +find_library(AMAVUTILS_LIBRARY + NAMES libamavutils.so + DOC "Path to Amlogic Audio Video Utils Library" + PATHS /usr/lib/aml_libs /usr/local/lib /usr/lib) +mark_as_advanced(AMAVUTILS_LIBRARY) + +find_library(AMADEC_LIBRARY + NAMES libamadec.so + DOC "Path to Amlogic Audio Decoder Library" + PATHS /usr/lib/aml_libs /usr/local/lib /usr/lib) +mark_as_advanced(AMADEC_LIBRARY) + +find_library(AMCODEC_LIBRARY + NAMES libamcodec.so + DOC "Path to Amlogic Video Codec Library" + PATHS /usr/lib/aml_libs /usr/local/lib /usr/lib) +mark_as_advanced(AMCODEC_LIBRARY) + +include(${CMAKE_ROOT}/Modules/FindPackageHandleStandardArgs.cmake) +FIND_PACKAGE_HANDLE_STANDARD_ARGS(Amlogic DEFAULT_MSG AMLOGIC_INCLUDE_DIR AMCODEC_LIBRARY AMADEC_LIBRARY AMAVUTILS_LIBRARY) + +set(AMLOGIC_LIBRARIES ${AMCODEC_LIBRARY} ${AMADEC_LIBRARY} ${AMAVUTILS_LIBRARY}) +set(AMLOGIC_INCLUDE_DIRS ${AMLOGIC_INCLUDE_DIR}) diff --git a/cmake/FindCEC.cmake b/cmake/FindCEC.cmake deleted file mode 100644 index 0f94c8f..0000000 --- a/cmake/FindCEC.cmake +++ /dev/null @@ -1,19 +0,0 @@ -# - Try to find CEC -# Once done this will define -# -# CEC_FOUND - system has libcec -# CEC_INCLUDE_DIRS - the libcec include directory -# CEC_LIBRARIES - The libcec libraries - -if(PKG_CONFIG_FOUND) - pkg_check_modules (CEC libcec>=3.0.0) -else() - find_path(CEC_INCLUDE_DIRS libcec/cec.h) - find_library(CEC_LIBRARIES cec) -endif() - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(CEC DEFAULT_MSG CEC_INCLUDE_DIRS CEC_LIBRARIES) - -list(APPEND CEC_DEFINITIONS -DHAVE_LIBCEC=1) -mark_as_advanced(CEC_INCLUDE_DIRS CEC_LIBRARIES CEC_DEFINITIONS) diff --git a/docs/README.pod b/docs/README.pod index 79712a1..51d2c4a 100644 --- a/docs/README.pod +++ b/docs/README.pod @@ -19,6 +19,10 @@ Create a mapping file for a gamepad. Pair this computer with the host. +=item B + +Unpair this computer with the host. + =item B Stream game from host to this computer. @@ -95,6 +99,11 @@ Change the network packetsize to I. The packetsize should the smaller than the MTU of the network. By default a safe value of 1024 is used. +=item B<-forcehevc> + +Request a h265/HEVC from the server. +Will still use h264 if server doesn't support HEVC. + =item B<-remote> Enable the optimizations for remote connections in GFE. @@ -132,7 +141,7 @@ To use a different gamepad mapping then the default the B<-mapping> should be sp =item B<-audio> [I] Use as audio output device. -The default value is 'sysdefault' +The default value is 'sysdefault' for ALSA and 'hdmi' for OMX on the Raspberry Pi. =back diff --git a/libgamestream/CMakeLists.txt b/libgamestream/CMakeLists.txt index a7896a5..97ded6f 100644 --- a/libgamestream/CMakeLists.txt +++ b/libgamestream/CMakeLists.txt @@ -5,12 +5,12 @@ find_package(OpenSSL REQUIRED) find_package(EXPAT REQUIRED) pkg_check_modules(AVAHI REQUIRED avahi-client) +pkg_check_modules(ENET REQUIRED libenet) aux_source_directory(./ GAMESTREAM_SRC_LIST) aux_source_directory(../third_party/h264bitstream GAMESTREAM_SRC_LIST) -aux_source_directory(../third_party/moonlight-common-c/limelight-common MOONLIGHT_COMMON_SRC_LIST) -aux_source_directory(../third_party/moonlight-common-c/limelight-common/OpenAES MOONLIGHT_COMMON_SRC_LIST) +aux_source_directory(../third_party/moonlight-common-c/src MOONLIGHT_COMMON_SRC_LIST) add_library(moonlight-common SHARED ${MOONLIGHT_COMMON_SRC_LIST}) @@ -21,8 +21,10 @@ target_link_libraries(gamestream moonlight-common) set_target_properties(gamestream PROPERTIES SOVERSION 0 VERSION ${MOONLIGHT_VERSION}) set_target_properties(moonlight-common PROPERTIES SOVERSION 0 VERSION ${MOONLIGHT_VERSION}) -target_include_directories(gamestream PRIVATE ../third_party/moonlight-common-c ../third_party/h264bitstream ${AVAHI_INCLUDE_DIRS} ${LIBUUID_INCLUDE_DIRS}) +target_include_directories(gamestream PRIVATE ../third_party/moonlight-common-c/src ../third_party/h264bitstream ${AVAHI_INCLUDE_DIRS} ${LIBUUID_INCLUDE_DIRS}) +target_include_directories(moonlight-common PRIVATE ${ENET_INCLUDE_DIRS}) target_link_libraries(gamestream ${CURL_LIBRARIES} ${OPENSSL_LIBRARIES} ${EXPAT_LIBRARIES} ${AVAHI_LIBRARIES} ${LIBUUID_LIBRARIES}) +target_link_libraries(moonlight-common ${ENET_LIBRARIES}) target_link_libraries(gamestream ${CMAKE_THREAD_LIBS_INIT} ${CMAKE_DL_LIBS}) diff --git a/libgamestream/client.c b/libgamestream/client.c index 0f0ecca..937cba3 100644 --- a/libgamestream/client.c +++ b/libgamestream/client.c @@ -1,7 +1,7 @@ /* * This file is part of Moonlight Embedded. * - * Copyright (C) 2015 Iwan Timmer + * Copyright (C) 2015-2016 Iwan Timmer * * Moonlight is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -23,7 +23,7 @@ #include "client.h" #include "errors.h" -#include "limelight-common/Limelight.h" +#include #include #include @@ -166,85 +166,117 @@ static int load_cert(const char* keyDirectory) { } static int load_server_status(PSERVER_DATA server) { - char *pairedText = NULL; - char *currentGameText = NULL; - char *versionText = NULL; - char *stateText = NULL; - char *heightText = NULL; - char *serverCodecModeSupportText = NULL; uuid_t uuid; char uuid_str[37]; - - int ret = GS_INVALID; + + int ret; char url[4096]; - uuid_generate_random(uuid); - uuid_unparse(uuid, uuid_str); - sprintf(url, "https://%s:47984/serverinfo?uniqueid=%s&uuid=%s", server->address, unique_id, uuid_str); + int i; - PHTTP_DATA data = http_create_data(); - if (data == NULL) { - ret = GS_OUT_OF_MEMORY; - goto cleanup; + i = 0; + do { + char *pairedText = NULL; + char *currentGameText = NULL; + char *versionText = NULL; + char *stateText = NULL; + char *heightText = NULL; + char *serverCodecModeSupportText = NULL; + + ret = GS_INVALID; + + uuid_generate_random(uuid); + uuid_unparse(uuid, uuid_str); + + // Modern GFE versions don't allow serverinfo to be fetched over HTTPS if the client + // is not already paired. Since we can't pair without knowing the server version, we + // make another request over HTTP if the HTTPS request fails. We can't just use HTTP + // for everything because it doesn't accurately tell us if we're paired. + sprintf(url, "%s://%s:%d/serverinfo?uniqueid=%s&uuid=%s", + i == 0 ? "https" : "http", server->address, i == 0 ? 47984 : 47989, unique_id, uuid_str); + + PHTTP_DATA data = http_create_data(); + if (data == NULL) { + ret = GS_OUT_OF_MEMORY; + goto cleanup; + } + if (http_request(url, data) != GS_OK) { + ret = GS_IO_ERROR; + goto cleanup; + } + + if (xml_search(data->memory, data->size, "currentgame", ¤tGameText) != GS_OK) { + goto cleanup; + } + + if (xml_search(data->memory, data->size, "PairStatus", &pairedText) != GS_OK) + goto cleanup; + + if (xml_search(data->memory, data->size, "appversion", &versionText) != GS_OK) + goto cleanup; + + if (xml_search(data->memory, data->size, "state", &stateText) != GS_OK) + goto cleanup; + + if (xml_search(data->memory, data->size, "Height", &heightText) != GS_OK) + goto cleanup; + + if (xml_search(data->memory, data->size, "ServerCodecModeSupport", &serverCodecModeSupportText) != GS_OK) + goto cleanup; + + if (xml_search(data->memory, data->size, "gputype", &server->gpuType) != GS_OK) + goto cleanup; + + if (xml_search(data->memory, data->size, "GfeVersion", &server->gfeVersion) != GS_OK) + goto cleanup; + + // These fields are present on all version of GFE that this client supports + if (!strlen(currentGameText) || !strlen(pairedText) || !strlen(versionText) || !strlen(stateText)) + goto cleanup; + + server->paired = pairedText != NULL && strcmp(pairedText, "1") == 0; + server->currentGame = currentGameText == NULL ? 0 : atoi(currentGameText); + server->supports4K = heightText != NULL && serverCodecModeSupportText != NULL && atoi(heightText) >= 2160; + server->serverMajorVersion = atoi(versionText); + if (strstr(stateText, "_SERVER_AVAILABLE")) { + // After GFE 2.8, current game remains set even after streaming + // has ended. We emulate the old behavior by forcing it to zero + // if streaming is not active. + server->currentGame = 0; + } + ret = GS_OK; + + cleanup: + if (data != NULL) + http_free_data(data); + + if (pairedText != NULL) + free(pairedText); + + if (currentGameText != NULL) + free(currentGameText); + + if (versionText != NULL) + free(versionText); + + if (heightText != NULL) + free(heightText); + + if (serverCodecModeSupportText != NULL) + free(serverCodecModeSupportText); + + i++; + } while (ret != GS_OK && i < 2); + + if (ret == GS_OK) { + if (server->serverMajorVersion > MAX_SUPPORTED_GFE_VERSION) { + gs_error = "Ensure you're running the latest version of Moonlight Embedded or downgrade GeForce Experience and try again"; + ret = GS_UNSUPPORTED_VERSION; + } else if (server->serverMajorVersion < MIN_SUPPORTED_GFE_VERSION) { + gs_error = "Moonlight Embedded requires a newer version of GeForce Experience. Please upgrade GFE on your PC and try again."; + ret = GS_UNSUPPORTED_VERSION; + } } - if (http_request(url, data) != GS_OK) { - ret = GS_IO_ERROR; - goto cleanup; - } - - if (xml_search(data->memory, data->size, "currentgame", ¤tGameText) != GS_OK) { - goto cleanup; - } - - if (xml_search(data->memory, data->size, "PairStatus", &pairedText) != GS_OK) - goto cleanup; - - if (xml_search(data->memory, data->size, "appversion", &versionText) != GS_OK) - goto cleanup; - - if (xml_search(data->memory, data->size, "state", &stateText) != GS_OK) - goto cleanup; - - if (xml_search(data->memory, data->size, "Height", &heightText) != GS_OK) - goto cleanup; - - if (xml_search(data->memory, data->size, "ServerCodecModeSupport", &serverCodecModeSupportText) != GS_OK) - goto cleanup; - - server->paired = pairedText != NULL && strcmp(pairedText, "1") == 0; - server->currentGame = currentGameText == NULL ? 0 : atoi(currentGameText); - server->supports4K = heightText != NULL && serverCodecModeSupportText != NULL && atoi(heightText) >= 2160; - char *versionSep = strstr(versionText, "."); - if (versionSep != NULL) { - *versionSep = 0; - } - server->serverMajorVersion = atoi(versionText); - if (strstr(stateText, "_SERVER_AVAILABLE")) { - // After GFE 2.8, current game remains set even after streaming - // has ended. We emulate the old behavior by forcing it to zero - // if streaming is not active. - server->currentGame = 0; - } - ret = GS_OK; - - cleanup: - if (data != NULL) - http_free_data(data); - - if (pairedText != NULL) - free(pairedText); - - if (currentGameText != NULL) - free(currentGameText); - - if (versionText != NULL) - free(versionText); - - if (heightText != NULL) - free(heightText); - - if (serverCodecModeSupportText != NULL) - free(serverCodecModeSupportText); return ret; } @@ -305,8 +337,53 @@ static int sign_it(const char *msg, size_t mlen, unsigned char **sig, size_t *sl return result; } +static bool verifySignature(const char *data, int dataLength, const char *signature, int signatureLength, const char *cert) { + X509* x509; + BIO* bio = BIO_new(BIO_s_mem()); + BIO_puts(bio, cert); + x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL); + + BIO_free(bio); + + if (!x509) { + return false; + } + + EVP_PKEY* pubKey = X509_get_pubkey(x509); + EVP_MD_CTX *mdctx = NULL; + mdctx = EVP_MD_CTX_create(); + EVP_DigestVerifyInit(mdctx, NULL, EVP_sha256(), NULL, pubKey); + EVP_DigestVerifyUpdate(mdctx, data, dataLength); + int result = EVP_DigestVerifyFinal(mdctx, signature, signatureLength); + + X509_free(x509); + EVP_PKEY_free(pubKey); + EVP_MD_CTX_destroy(mdctx); + + return result > 0; +} + +int gs_unpair(PSERVER_DATA server) { + int ret = GS_OK; + char url[4096]; + uuid_t uuid; + char uuid_str[37]; + PHTTP_DATA data = http_create_data(); + if (data == NULL) + return GS_OUT_OF_MEMORY; + + uuid_generate_random(uuid); + uuid_unparse(uuid, uuid_str); + sprintf(url, "http://%s:47989/unpair?uniqueid=%s&uuid=%s", server->address, unique_id, uuid_str); + ret = http_request(url, data); + + http_free_data(data); + return ret; +} + int gs_pair(PSERVER_DATA server, char* pin) { int ret = GS_OK; + char* result = NULL; char url[4096]; uuid_t uuid; char uuid_str[37]; @@ -335,12 +412,45 @@ int gs_pair(PSERVER_DATA server, char* pin) { else if ((ret = http_request(url, data)) != GS_OK) goto cleanup; + if ((ret = xml_search(data->memory, data->size, "paired", &result)) != GS_OK) + goto cleanup; + + if (strcmp(result, "1") != 0) { + gs_error = "Pairing failed"; + ret = GS_FAILED; + goto cleanup; + } + + free(result); + result = NULL; + if ((ret = xml_search(data->memory, data->size, "plaincert", &result)) != GS_OK) + goto cleanup; + + if (strlen(result)/2 > 8191) { + gs_error = "Server certificate too big"; + ret = GS_FAILED; + goto cleanup; + } + + char plaincert[8192]; + for (int count = 0; count < strlen(result); count += 2) { + sscanf(&result[count], "%2hhx", &plaincert[count / 2]); + } + plaincert[strlen(result)/2] = '\0'; + printf("%d / %d\n", strlen(result)/2, strlen(plaincert)); + unsigned char salt_pin[20]; - unsigned char aes_key_hash[20]; + unsigned char aes_key_hash[32]; AES_KEY enc_key, dec_key; memcpy(salt_pin, salt_data, 16); memcpy(salt_pin+16, pin, 4); - SHA1(salt_pin, 20, aes_key_hash); + + int hash_length = server->serverMajorVersion >= 7 ? 32 : 20; + if (server->serverMajorVersion >= 7) + SHA256(salt_pin, 20, aes_key_hash); + else + SHA1(salt_pin, 20, aes_key_hash); + AES_set_encrypt_key((unsigned char *)aes_key_hash, 128, &enc_key); AES_set_decrypt_key((unsigned char *)aes_key_hash, 128, &dec_key); @@ -357,7 +467,19 @@ int gs_pair(PSERVER_DATA server, char* pin) { if ((ret = http_request(url, data)) != GS_OK) goto cleanup; - char *result; + free(result); + result = NULL; + if ((ret = xml_search(data->memory, data->size, "paired", &result)) != GS_OK) + goto cleanup; + + if (strcmp(result, "1") != 0) { + gs_error = "Pairing failed"; + ret = GS_FAILED; + goto cleanup; + } + + free(result); + result = NULL; if (xml_search(data->memory, data->size, "challengeresponse", &result) != GS_OK) { ret = GS_INVALID; goto cleanup; @@ -368,7 +490,6 @@ int gs_pair(PSERVER_DATA server, char* pin) { for (int count = 0; count < strlen(result); count += 2) { sscanf(&result[count], "%2hhx", &challenge_response_data_enc[count / 2]); } - free(result); for (int i = 0; i < 48; i += 16) { AES_decrypt(&challenge_response_data_enc[i], &challenge_response_data[i], &dec_key); @@ -381,10 +502,13 @@ int gs_pair(PSERVER_DATA server, char* pin) { char challenge_response_hash[32]; char challenge_response_hash_enc[32]; char challenge_response_hex[65]; - memcpy(challenge_response, challenge_response_data + 20, 16); + memcpy(challenge_response, challenge_response_data + hash_length, 16); memcpy(challenge_response + 16, cert->signature->data, 256); memcpy(challenge_response + 16 + 256, client_secret_data, 16); - SHA1(challenge_response, 16 + 256 + 16, challenge_response_hash); + if (server->serverMajorVersion >= 7) + SHA256(challenge_response, 16 + 256 + 16, challenge_response_hash); + else + SHA1(challenge_response, 16 + 256 + 16, challenge_response_hash); for (int i = 0; i < 32; i += 16) { AES_encrypt(&challenge_response_hash[i], &challenge_response_hash_enc[i], &enc_key); @@ -397,11 +521,35 @@ int gs_pair(PSERVER_DATA server, char* pin) { if ((ret = http_request(url, data)) != GS_OK) goto cleanup; + free(result); + result = NULL; + if ((ret = xml_search(data->memory, data->size, "paired", &result)) != GS_OK) + goto cleanup; + + if (strcmp(result, "1") != 0) { + gs_error = "Pairing failed"; + ret = GS_FAILED; + goto cleanup; + } + + free(result); + result = NULL; if (xml_search(data->memory, data->size, "pairingsecret", &result) != GS_OK) { ret = GS_INVALID; goto cleanup; } + char pairing_secret[16 + 256]; + for (int count = 0; count < strlen(result); count += 2) { + sscanf(&result[count], "%2hhx", &pairing_secret[count / 2]); + } + + if (!verifySignature(pairing_secret, 16, pairing_secret+16, 256, plaincert)) { + gs_error = "MITM attack detected"; + ret = GS_FAILED; + goto cleanup; + } + unsigned char *signature = NULL; size_t s_len; if (sign_it(client_secret_data, 16, &signature, &s_len, privateKey) != GS_OK) { @@ -422,15 +570,43 @@ int gs_pair(PSERVER_DATA server, char* pin) { if ((ret = http_request(url, data)) != GS_OK) goto cleanup; + free(result); + result = NULL; + if ((ret = xml_search(data->memory, data->size, "paired", &result)) != GS_OK) + goto cleanup; + + if (strcmp(result, "1") != 0) { + gs_error = "Pairing failed"; + ret = GS_FAILED; + goto cleanup; + } + uuid_generate_random(uuid); uuid_unparse(uuid, uuid_str); sprintf(url, "https://%s:47984/pair?uniqueid=%s&uuid=%s&devicename=roth&updateState=1&phrase=pairchallenge", server->address, unique_id, uuid_str); if ((ret = http_request(url, data)) != GS_OK) goto cleanup; + free(result); + result = NULL; + if ((ret = xml_search(data->memory, data->size, "paired", &result)) != GS_OK) + goto cleanup; + + if (strcmp(result, "1") != 0) { + gs_error = "Pairing failed"; + ret = GS_FAILED; + goto cleanup; + } + server->paired = true; cleanup: + if (ret != GS_OK) + gs_unpair(server); + + if (result != NULL) + free(result); + http_free_data(data); return ret; @@ -458,7 +634,9 @@ int gs_applist(PSERVER_DATA server, PAPP_LIST *list) { } int gs_start_app(PSERVER_DATA server, STREAM_CONFIGURATION *config, int appId, bool sops, bool localaudio) { + int ret = GS_OK; uuid_t uuid; + char* result = NULL; char uuid_str[37]; if (config->height >= 2160 && !server->supports4K) @@ -469,7 +647,7 @@ int gs_start_app(PSERVER_DATA server, STREAM_CONFIGURATION *config, int appId, b srand(time(NULL)); char url[4096]; - u_int32_t rikeyid = 1; + u_int32_t rikeyid = 0; char rikey_hex[33]; bytes_to_hex(config->remoteInputAesKey, rikey_hex, 16); @@ -486,18 +664,33 @@ int gs_start_app(PSERVER_DATA server, STREAM_CONFIGURATION *config, int appId, b } else sprintf(url, "https://%s:47984/resume?uniqueid=%s&uuid=%s&rikey=%s&rikeyid=%d", server->address, unique_id, uuid_str, rikey_hex, rikeyid); - int ret = http_request(url, data); - if (ret == GS_OK) + if ((ret = http_request(url, data)) == GS_OK) server->currentGame = appId; + else + goto cleanup; + + if ((ret = xml_search(data->memory, data->size, "gamesession", &result)) != GS_OK) + goto cleanup; + + if (!strcmp(result, "0")) { + ret = GS_FAILED; + goto cleanup; + } + + cleanup: + if (result != NULL) + free(result); http_free_data(data); return ret; } int gs_quit_app(PSERVER_DATA server) { + int ret = GS_OK; char url[4096]; uuid_t uuid; char uuid_str[37]; + char* result = NULL; PHTTP_DATA data = http_create_data(); if (data == NULL) return GS_OUT_OF_MEMORY; @@ -505,7 +698,20 @@ int gs_quit_app(PSERVER_DATA server) { uuid_generate_random(uuid); uuid_unparse(uuid, uuid_str); sprintf(url, "https://%s:47984/cancel?uniqueid=%s&uuid=%s", server->address, unique_id, uuid_str); - int ret = http_request(url, data); + if ((ret = http_request(url, data)) != GS_OK) + goto cleanup; + + if ((ret = xml_search(data->memory, data->size, "cancel", &result)) != GS_OK) + goto cleanup; + + if (strcmp(result, "0") == 0) { + ret = GS_FAILED; + goto cleanup; + } + + cleanup: + if (result != NULL) + free(result); http_free_data(data); return ret; diff --git a/libgamestream/client.h b/libgamestream/client.h index 826a6c9..9a8fc91 100644 --- a/libgamestream/client.h +++ b/libgamestream/client.h @@ -1,7 +1,7 @@ /* * This file is part of Moonlight Embedded. * - * Copyright (C) 2015 Iwan Timmer + * Copyright (C) 2015, 2016 Iwan Timmer * * Moonlight is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,12 +21,17 @@ #include "xml.h" -#include "limelight-common/Limelight.h" +#include #include +#define MIN_SUPPORTED_GFE_VERSION 3 +#define MAX_SUPPORTED_GFE_VERSION 7 + typedef struct _SERVER_DATA { const char* address; + char* gpuType; + char* gfeVersion; bool paired; bool supports4K; int currentGame; @@ -36,5 +41,6 @@ typedef struct _SERVER_DATA { int gs_init(PSERVER_DATA server, const char *keyDirectory); int gs_start_app(PSERVER_DATA server, PSTREAM_CONFIGURATION config, int appId, bool sops, bool localaudio); int gs_applist(PSERVER_DATA server, PAPP_LIST *app_list); +int gs_unpair(PSERVER_DATA server); int gs_pair(PSERVER_DATA server, char* pin); int gs_quit_app(PSERVER_DATA server); diff --git a/libgamestream/errors.h b/libgamestream/errors.h index 441aea3..99e3187 100644 --- a/libgamestream/errors.h +++ b/libgamestream/errors.h @@ -1,7 +1,7 @@ /* * This file is part of Moonlight Embedded. * - * Copyright (C) 2015 Iwan Timmer + * Copyright (C) 2015, 2016 Iwan Timmer * * Moonlight is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -26,5 +26,6 @@ #define GS_WRONG_STATE -4 #define GS_IO_ERROR -5 #define GS_NOT_SUPPORTED_4K -6 +#define GS_UNSUPPORTED_VERSION -7 const char* gs_error; diff --git a/libgamestream/sps.h b/libgamestream/sps.h index bd2cf46..9123b6f 100644 --- a/libgamestream/sps.h +++ b/libgamestream/sps.h @@ -17,7 +17,7 @@ * along with Moonlight; if not, see . */ -#include "limelight-common/Limelight.h" +#include #define GS_SPS_BITSTREAM_FIXUP 0x01 #define GS_SPS_BASELINE_HACK 0x02 diff --git a/moonlight.conf b/moonlight.conf index 79ccdbb..a800d6c 100644 --- a/moonlight.conf +++ b/moonlight.conf @@ -17,6 +17,9 @@ ## Size of network packets should be lower than MTU #packetsize = 1024 +## Use of h265/HEVC video codec +#h265 = false + ## Default started application on host #app = Steam @@ -42,6 +45,7 @@ ## Select the audio and video decoder to use ## default - autodetect +## aml - hardware video decoder for ODROID-C1/C2 ## omx - hardware video decoder for Raspberry Pi ## imx - hardware video decoder for i.MX6 devices ## sdl - software decoder diff --git a/src/audio.h b/src/audio.h index 8236e2a..3981212 100644 --- a/src/audio.h +++ b/src/audio.h @@ -19,7 +19,7 @@ #include -#include "limelight-common/Limelight.h" +#include extern const char* audio_device; @@ -31,3 +31,6 @@ extern AUDIO_RENDERER_CALLBACKS audio_callbacks_sdl; extern AUDIO_RENDERER_CALLBACKS audio_callbacks_pulse; bool audio_pulse_init(); #endif +#ifdef HAVE_PI +extern AUDIO_RENDERER_CALLBACKS audio_callbacks_omx; +#endif diff --git a/src/audio/alsa.c b/src/audio/alsa.c index 3999b2c..273ce82 100644 --- a/src/audio/alsa.c +++ b/src/audio/alsa.c @@ -1,7 +1,7 @@ /* * This file is part of Moonlight Embedded. * - * Copyright (C) 2015 Iwan Timmer + * Copyright (C) 2015, 2016 Iwan Timmer * * Moonlight is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -28,8 +28,6 @@ #define MAX_CHANNEL_COUNT 6 #define FRAME_SIZE 240 -const char* audio_device = "sysdefault"; - static snd_pcm_t *handle; static OpusMSDecoder* decoder; static short pcmBuffer[FRAME_SIZE * MAX_CHANNEL_COUNT]; @@ -64,6 +62,9 @@ static void alsa_renderer_init(int audioConfiguration, POPUS_MULTISTREAM_CONFIGU snd_pcm_uframes_t buffer_size = 12 * period_size; unsigned int sampleRate = opusConfig->sampleRate; + if (audio_device == NULL) + audio_device = "sysdefault"; + /* Open PCM device for playback. */ CHECK_RETURN(snd_pcm_open(&handle, audio_device, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK)) diff --git a/src/audio/omx.c b/src/audio/omx.c new file mode 100644 index 0000000..bf8f7d3 --- /dev/null +++ b/src/audio/omx.c @@ -0,0 +1,208 @@ +/* + * This file is part of Moonlight Embedded. + * + * Copyright (C) 2015, 2016 Iwan Timmer + * + * Moonlight is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * Moonlight is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Moonlight; if not, see . + */ + +#include "../audio.h" + +#include + +#include +#include "bcm_host.h" +#include "ilclient.h" + +#define MAX_CHANNEL_COUNT 6 +#define FRAME_SIZE 240 + +static OpusMSDecoder* decoder; +ILCLIENT_T* handle; +COMPONENT_T* component; +static OMX_BUFFERHEADERTYPE *buf; +static short pcmBuffer[FRAME_SIZE * MAX_CHANNEL_COUNT]; +static int channelCount; + +static void omx_renderer_init(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig) { + int rc, error; + OMX_ERRORTYPE err; + unsigned char omxMapping[6]; + char* componentName = "audio_render"; + + channelCount = opusConfig->channelCount; + /* The supplied mapping array has order: FL-FR-C-LFE-RL-RR + * OMX expects the order: FL-FR-LFE-C-RL-RR + * We need copy the mapping locally and swap the channels around. + */ + memcpy(omxMapping, opusConfig->mapping, sizeof(omxMapping)); + if (opusConfig->channelCount > 2) { + omxMapping[2] = opusConfig->mapping[3]; + omxMapping[3] = opusConfig->mapping[2]; + } + + decoder = opus_multistream_decoder_create(opusConfig->sampleRate, + opusConfig->channelCount, + opusConfig->streams, + opusConfig->coupledStreams, + omxMapping, + &rc); + + handle = ilclient_init(); + if (handle == NULL) { + fprintf(stderr, "IL client init failed\n"); + exit(1); + } + + if (ilclient_create_component(handle, &component, componentName, ILCLIENT_DISABLE_ALL_PORTS | ILCLIENT_ENABLE_INPUT_BUFFERS) != 0) { + fprintf(stderr, "Component create failed\n"); + exit(1); + } + + if (ilclient_change_component_state(component, OMX_StateIdle)!= 0) { + fprintf(stderr, "Couldn't change state to Idle\n"); + exit(1); + } + + // must be before we enable buffers + OMX_AUDIO_PARAM_PORTFORMATTYPE audioPortFormat; + memset(&audioPortFormat, 0, sizeof(OMX_AUDIO_PARAM_PORTFORMATTYPE)); + audioPortFormat.nSize = sizeof(OMX_AUDIO_PARAM_PORTFORMATTYPE); + audioPortFormat.nVersion.nVersion = OMX_VERSION; + + audioPortFormat.nPortIndex = 100; + + OMX_GetParameter(ilclient_get_handle(component), OMX_IndexParamAudioPortFormat, &audioPortFormat); + + audioPortFormat.eEncoding = OMX_AUDIO_CodingPCM; + OMX_SetParameter(ilclient_get_handle(component), OMX_IndexParamAudioPortFormat, &audioPortFormat); + + OMX_AUDIO_PARAM_PCMMODETYPE sPCMMode; + + memset(&sPCMMode, 0, sizeof(OMX_AUDIO_PARAM_PCMMODETYPE)); + sPCMMode.nSize = sizeof(OMX_AUDIO_PARAM_PCMMODETYPE); + sPCMMode.nVersion.nVersion = OMX_VERSION; + sPCMMode.nPortIndex = 100; + sPCMMode.nChannels = channelCount; + sPCMMode.eNumData = OMX_NumericalDataSigned; + sPCMMode.eEndian = OMX_EndianLittle; + sPCMMode.nSamplingRate = opusConfig->sampleRate; + sPCMMode.bInterleaved = OMX_TRUE; + sPCMMode.nBitPerSample = 16; + sPCMMode.ePCMMode = OMX_AUDIO_PCMModeLinear; + + switch(channelCount) { + case 1: + sPCMMode.eChannelMapping[0] = OMX_AUDIO_ChannelCF; + break; + case 8: + sPCMMode.eChannelMapping[7] = OMX_AUDIO_ChannelRS; + case 7: + sPCMMode.eChannelMapping[6] = OMX_AUDIO_ChannelLS; + case 6: + sPCMMode.eChannelMapping[5] = OMX_AUDIO_ChannelRR; + case 5: + sPCMMode.eChannelMapping[4] = OMX_AUDIO_ChannelLR; + case 4: + sPCMMode.eChannelMapping[3] = OMX_AUDIO_ChannelLFE; + case 3: + sPCMMode.eChannelMapping[2] = OMX_AUDIO_ChannelCF; + case 2: + sPCMMode.eChannelMapping[1] = OMX_AUDIO_ChannelRF; + sPCMMode.eChannelMapping[0] = OMX_AUDIO_ChannelLF; + break; + } + + err = OMX_SetParameter(ilclient_get_handle(component), OMX_IndexParamAudioPcm, &sPCMMode); + if(err != OMX_ErrorNone){ + fprintf(stderr, "PCM mode unsupported\n"); + return; + } + OMX_CONFIG_BRCMAUDIODESTINATIONTYPE arDest; + + if (audio_device == NULL) + audio_device = "hdmi"; + + if (audio_device && strlen(audio_device) < sizeof(arDest.sName)) { + memset(&arDest, 0, sizeof(OMX_CONFIG_BRCMAUDIODESTINATIONTYPE)); + arDest.nSize = sizeof(OMX_CONFIG_BRCMAUDIODESTINATIONTYPE); + arDest.nVersion.nVersion = OMX_VERSION; + + strcpy((char *)arDest.sName, audio_device); + + err = OMX_SetParameter(ilclient_get_handle(component), OMX_IndexConfigBrcmAudioDestination, &arDest); + if (err != OMX_ErrorNone) { + fprintf(stderr, "Error on setting audio destination\nomx option must be set to hdmi or local\n"); + exit(1); + } + } + + // input port + ilclient_enable_port_buffers(component, 100, NULL, NULL, NULL); + ilclient_enable_port(component, 100); + + err = ilclient_change_component_state(component, OMX_StateExecuting); + if (err < 0) { + fprintf(stderr, "Couldn't change state to Executing\n"); + exit(1); + } +} + +static void omx_renderer_cleanup() { + if (decoder != NULL) + opus_multistream_decoder_destroy(decoder); + if (handle != NULL) { + if((buf = ilclient_get_input_buffer(component, 100, 1)) == NULL){ + fprintf(stderr, "Can't get audio buffer\n"); + exit(EXIT_FAILURE); + } + + buf->nFilledLen = 0; + buf->nFlags = OMX_BUFFERFLAG_TIME_UNKNOWN | OMX_BUFFERFLAG_EOS; + + if(OMX_EmptyThisBuffer(ILC_GET_HANDLE(component), buf) != OMX_ErrorNone){ + fprintf(stderr, "Can't empty audio buffer\n"); + return; + } + + ilclient_disable_port_buffers(component, 100, NULL, NULL, NULL); + ilclient_change_component_state(component, OMX_StateIdle); + ilclient_change_component_state(component, OMX_StateLoaded); + } +} + +static void omx_renderer_decode_and_play_sample(char* data, int length) { + int decodeLen = opus_multistream_decode(decoder, data, length, pcmBuffer, FRAME_SIZE, 0); + if (decodeLen > 0) { + buf = ilclient_get_input_buffer(component, 100, 1); + buf->nOffset = 0; + buf->nFlags = OMX_BUFFERFLAG_TIME_UNKNOWN; + int bufLength = decodeLen * sizeof(short) * channelCount; + memcpy(buf->pBuffer, pcmBuffer, bufLength); + buf->nFilledLen = bufLength; + int r = OMX_EmptyThisBuffer(ilclient_get_handle(component), buf); + if (r != OMX_ErrorNone) { + fprintf(stderr, "Empty buffer error\n"); + } + } else { + printf("Opus error from decode: %d\n", decodeLen); + } +} + +AUDIO_RENDERER_CALLBACKS audio_callbacks_omx = { + .init = omx_renderer_init, + .cleanup = omx_renderer_cleanup, + .decodeAndPlaySample = omx_renderer_decode_and_play_sample, + .capabilities = CAPABILITY_DIRECT_SUBMIT, +}; diff --git a/src/config.c b/src/config.c index 52dae3b..d19cf32 100644 --- a/src/config.c +++ b/src/config.c @@ -1,7 +1,7 @@ /* * This file is part of Moonlight Embedded. * - * Copyright (C) 2015 Iwan Timmer + * Copyright (C) 2015, 2016 Iwan Timmer * * Moonlight is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -38,6 +38,7 @@ bool inputAdded = false; static bool mapped = true; +const char* audio_device = NULL; static struct option long_options[] = { {"720", no_argument, NULL, 'a'}, @@ -63,6 +64,8 @@ static struct option long_options[] = { {"surround", no_argument, NULL, 'u'}, {"fps", required_argument, NULL, 'v'}, {"forcehw", no_argument, NULL, 'w'}, + {"forcehevc", no_argument, NULL, 'x'}, + {"unsupported", no_argument, NULL, 'y'}, {0, 0, 0, 0}, }; @@ -199,6 +202,13 @@ static void parse_argument(int c, char* value, PCONFIGURATION config) { break; case 'w': config->forcehw = true; + break; + case 'x': + config->stream.supportsHevc = true; + break; + case 'y': + config->unsupported_version = true; + break; case 1: if (config->action == NULL) config->action = value; @@ -280,6 +290,7 @@ void config_parse(int argc, char* argv[], PCONFIGURATION config) { config->stream.packetSize = 1024; config->stream.streamingRemotely = 0; config->stream.audioConfiguration = AUDIO_CONFIGURATION_STEREO; + config->stream.supportsHevc = false; config->platform = "default"; config->app = "Steam"; @@ -289,6 +300,7 @@ void config_parse(int argc, char* argv[], PCONFIGURATION config) { config->sops = true; config->localaudio = false; config->fullscreen = true; + config->unsupported_version = false; config->inputsCount = 0; config->mapping = get_path("mappings/default.conf", getenv("XDG_DATA_DIRS")); @@ -306,7 +318,7 @@ void config_parse(int argc, char* argv[], PCONFIGURATION config) { } else { int option_index = 0; int c; - while ((c = getopt_long_only(argc, argv, "-abc:d:efg:h:i:j:k:lm:no:p:q:r:stuv:w", long_options, &option_index)) != -1) { + while ((c = getopt_long_only(argc, argv, "-abc:d:efg:h:i:j:k:lm:no:p:q:r:stuv:w:xy", long_options, &option_index)) != -1) { parse_argument(c, optarg, config); } } diff --git a/src/config.h b/src/config.h index 9b88b5c..cb448da 100644 --- a/src/config.h +++ b/src/config.h @@ -1,7 +1,7 @@ /* * This file is part of Moonlight Embedded. * - * Copyright (C) 2015 Iwan Timmer + * Copyright (C) 2015, 2016 Iwan Timmer * * Moonlight is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,7 +17,7 @@ * along with Moonlight; if not, see . */ -#include "limelight-common/Limelight.h" +#include #include @@ -41,6 +41,7 @@ typedef struct _CONFIGURATION { bool localaudio; bool fullscreen; bool forcehw; + bool unsupported_version; struct input_config inputs[MAX_INPUTS]; int inputsCount; } CONFIGURATION, *PCONFIGURATION; diff --git a/src/connection.h b/src/connection.h index e11537c..8b985b6 100644 --- a/src/connection.h +++ b/src/connection.h @@ -17,6 +17,6 @@ * along with Moonlight; if not, see . */ -#include "limelight-common/Limelight.h" +#include extern CONNECTION_LISTENER_CALLBACKS connection_callbacks; diff --git a/src/input/cec.c b/src/input/cec.c index 854a0e2..8c3892e 100644 --- a/src/input/cec.c +++ b/src/input/cec.c @@ -19,7 +19,7 @@ #ifdef HAVE_LIBCEC -#include "limelight-common/Limelight.h" +#include #include diff --git a/src/input/evdev.c b/src/input/evdev.c index 245105f..9e42a5c 100644 --- a/src/input/evdev.c +++ b/src/input/evdev.c @@ -24,7 +24,7 @@ #include "mapping.h" #include "libevdev/libevdev.h" -#include "limelight-common/Limelight.h" +#include #include #include @@ -106,9 +106,9 @@ static short evdev_convert_value(struct input_event *ev, struct input_device *de else if (ev->value < parms->min) return reverse?SHRT_MAX:SHRT_MIN; else if (reverse) - return (parms->max - (ev->valueavg?parms->flat*2:0) - ev->value) * (SHRT_MAX-SHRT_MIN) / (parms->max-parms->min-parms->flat*2) + SHRT_MIN; + return (long long)(parms->max - (ev->valueavg?parms->flat*2:0) - ev->value) * (SHRT_MAX-SHRT_MIN) / (parms->max-parms->min-parms->flat*2) + SHRT_MIN; else - return (ev->value - (ev->value>parms->avg?parms->flat*2:0) - parms->min) * (SHRT_MAX-SHRT_MIN) / (parms->max-parms->min-parms->flat*2) + SHRT_MIN; + return (long long)(ev->value - (ev->value>parms->avg?parms->flat*2:0) - parms->min) * (SHRT_MAX-SHRT_MIN) / (parms->max-parms->min-parms->flat*2) + SHRT_MIN; } static char evdev_convert_value_byte(struct input_event *ev, struct input_device *dev, struct input_abs_parms *parms) { diff --git a/src/input/sdlinput.c b/src/input/sdlinput.c index da6a6b7..2c95fb1 100644 --- a/src/input/sdlinput.c +++ b/src/input/sdlinput.c @@ -22,7 +22,7 @@ #include "sdlinput.h" #include "../sdl.h" -#include "limelight-common/Limelight.h" +#include #define ACTION_MODIFIERS (MODIFIER_SHIFT|MODIFIER_ALT|MODIFIER_CTRL) #define QUIT_KEY SDLK_q diff --git a/src/main.c b/src/main.c index 7459cf0..99783c7 100644 --- a/src/main.c +++ b/src/main.c @@ -1,7 +1,7 @@ /* * This file is part of Moonlight Embedded. * - * Copyright (C) 2015 Iwan Timmer + * Copyright (C) 2015, 2016 Iwan Timmer * * Moonlight is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -33,7 +33,7 @@ #include "input/cec.h" #include "input/sdlinput.h" -#include "limelight-common/Limelight.h" +#include #include #include @@ -121,6 +121,7 @@ static void help() { printf("\n Actions\n\n"); printf("\tmap\t\t\tCreate mapping file for gamepad\n"); printf("\tpair\t\t\tPair device with computer\n"); + printf("\tunpair\t\t\tUnpair device with computer\n"); printf("\tstream\t\t\tStream computer to device\n"); printf("\tlist\t\t\tList available games and applications\n"); printf("\tquit\t\t\tQuit the application or game being streamed\n"); @@ -137,6 +138,7 @@ static void help() { printf("\t-60fps\t\t\tUse 60fps [default]\n"); printf("\t-bitrate \tSpecify the bitrate in Kbps\n"); printf("\t-packetsize \tSpecify the maximum packetsize in bytes\n"); + printf("\t-forcehevc\t\tUse high efficiency video decoding (HEVC)\n"); printf("\t-remote\t\t\tEnable remote optimizations\n"); printf("\t-app \t\tName of app to stream\n"); printf("\t-nosops\t\t\tDon't allow GFE to modify game settings\n"); @@ -151,7 +153,7 @@ static void help() { printf("\n I/O options\n\n"); printf("\t-mapping \t\tUse as gamepad mapping configuration file (use before -input)\n"); printf("\t-input \t\tUse as input. Can be used multiple times\n"); - printf("\t-audio \t\tUse as ALSA audio output device (default sysdefault)\n"); + printf("\t-audio \t\tUse as audio output device\n"); printf("\t-forcehw \t\tTry to use video hardware acceleration\n"); #endif printf("\nUse Ctrl+Alt+Shift+Q to exit streaming session\n\n"); @@ -179,6 +181,7 @@ int main(int argc, char* argv[]) { fprintf(stderr, "Platform '%s' not found\n", config.platform); exit(-1); } + config.stream.supportsHevc = config.stream.supportsHevc || platform_supports_hevc(system); if (strcmp("map", config.action) == 0) { if (config.address == NULL) { @@ -200,6 +203,7 @@ int main(int argc, char* argv[]) { exit(-1); } config.address[0] = 0; + printf("Searching for server...\n"); gs_discover_server(config.address); if (config.address[0] == 0) { fprintf(stderr, "Autodiscovery failed. Specify an IP address next time.\n"); @@ -223,11 +227,18 @@ int main(int argc, char* argv[]) { } else if (ret == GS_INVALID) { fprintf(stderr, "Invalid data received from server: %s\n", config.address, gs_error); exit(-1); + } else if (ret == GS_UNSUPPORTED_VERSION) { + if (!config.unsupported_version) { + fprintf(stderr, "Unsupported version: %s\n", gs_error); + exit(-1); + } } else if (ret != GS_OK) { fprintf(stderr, "Can't connect to server %s\n", config.address); exit(-1); } + printf("NVIDIA %s, GFE %s (protocol version %d)\n", server.gpuType, server.gfeVersion, server.serverMajorVersion); + if (strcmp("list", config.action) == 0) { pair_check(&server); applist(&server); @@ -260,6 +271,12 @@ int main(int argc, char* argv[]) { } else { printf("Succesfully paired\n"); } + } else if (strcmp("unpair", config.action) == 0) { + if (gs_unpair(&server) != GS_OK) { + fprintf(stderr, "Failed to unpair to server: %s\n", gs_error); + } else { + printf("Succesfully unpaired\n"); + } } else if (strcmp("quit", config.action) == 0) { pair_check(&server); gs_quit_app(&server); diff --git a/src/platform.c b/src/platform.c index 3d4a81f..b03b68c 100644 --- a/src/platform.c +++ b/src/platform.c @@ -1,7 +1,7 @@ /* * This file is part of Moonlight Embedded. * - * Copyright (C) 2015 Iwan Timmer + * Copyright (C) 2015, 2016 Iwan Timmer * * Moonlight is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,9 +22,9 @@ #include "platform.h" #include "audio.h" -#include #include #include +#include #include typedef bool(*ImxInit)(); @@ -48,6 +48,13 @@ enum platform platform_check(char* name) { return PI; } #endif + #ifdef HAVE_AML + if (std || strcmp(name, "aml") == 0) { + void *handle = dlopen("libmoonlight-aml.so", RTLD_NOW | RTLD_GLOBAL); + if (handle != NULL && access("/dev/amvideo", F_OK) != -1) + return AML; + } + #endif #ifdef HAVE_SDL if (std || strcmp(name, "sdl") == 0) return SDL; @@ -73,6 +80,10 @@ DECODER_RENDERER_CALLBACKS* platform_get_video(enum platform system) { case PI: return (PDECODER_RENDERER_CALLBACKS) dlsym(RTLD_DEFAULT, "decoder_callbacks_pi"); #endif + #ifdef HAVE_AML + case AML: + return (PDECODER_RENDERER_CALLBACKS) dlsym(RTLD_DEFAULT, "decoder_callbacks_aml"); + #endif #ifdef HAVE_FAKE case FAKE: return &decoder_callbacks_fake; @@ -87,6 +98,11 @@ AUDIO_RENDERER_CALLBACKS* platform_get_audio(enum platform system) { case SDL: return &audio_callbacks_sdl; #endif + #ifdef HAVE_PI + case PI: + if (audio_device == NULL || strcmp(audio_device, "local") == 0 || strcmp(audio_device, "hdmi") == 0) + return (PAUDIO_RENDERER_CALLBACKS) dlsym(RTLD_DEFAULT, "audio_callbacks_omx"); + #endif default: #ifdef HAVE_PULSE if (audio_pulse_init()) @@ -96,3 +112,11 @@ AUDIO_RENDERER_CALLBACKS* platform_get_audio(enum platform system) { } return NULL; } + +bool platform_supports_hevc(enum platform system) { + switch (system) { + case AML: + return true; + } + return false; +} diff --git a/src/platform.h b/src/platform.h index ac08228..93dab74 100644 --- a/src/platform.h +++ b/src/platform.h @@ -1,7 +1,7 @@ /* * This file is part of Moonlight Embedded. * - * Copyright (C) 2015 Iwan Timmer + * Copyright (C) 2015, 2016 Iwan Timmer * * Moonlight is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,19 +17,21 @@ * along with Moonlight; if not, see . */ -#include "limelight-common/Limelight.h" +#include #include +#include #include #include #define IS_EMBEDDED(SYSTEM) SYSTEM != SDL -enum platform { NONE, SDL, PI, IMX, FAKE }; +enum platform { NONE, SDL, PI, IMX, AML, FAKE }; enum platform platform_check(char*); PDECODER_RENDERER_CALLBACKS platform_get_video(enum platform system); PAUDIO_RENDERER_CALLBACKS platform_get_audio(enum platform system); +bool platform_supports_hevc(enum platform system); #ifdef HAVE_FAKE extern DECODER_RENDERER_CALLBACKS decoder_callbacks_fake; diff --git a/src/sdl.c b/src/sdl.c index b48999e..6552748 100644 --- a/src/sdl.c +++ b/src/sdl.c @@ -22,7 +22,7 @@ #include "sdl.h" #include "input/sdlinput.h" -#include "limelight-common/Limelight.h" +#include static bool done; static int fullscreen_flags; diff --git a/src/video/aml.c b/src/video/aml.c new file mode 100644 index 0000000..704ab45 --- /dev/null +++ b/src/video/aml.c @@ -0,0 +1,120 @@ +/* + * This file is part of Moonlight Embedded. + * + * Copyright (C) 2015, 2016 Iwan Timmer + * Copyright (C) 2016 OtherCrashOverride, Daniel Mehrwald + * + * Moonlight is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * Moonlight is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Moonlight; if not, see . + */ + +#include + +#include +#include +#include +#include +#include +#include +#include + +static codec_para_t codecParam = { 0 }; +static const size_t SYNC_OUTSIDE = (2); + +static int osd_blank(char *path,int cmd) { + int fd; + char bcmd[16]; + + fd = open(path, O_CREAT|O_RDWR | O_TRUNC, 0644); + + if(fd>=0) { + sprintf(bcmd,"%d",cmd); + int ret = write(fd,bcmd,strlen(bcmd)); + if (ret < 0) { + printf("osd_blank error during write: %x\n", ret); + } + close(fd); + return 0; + } + + return -1; +} + +void aml_setup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) { + osd_blank("/sys/class/graphics/fb0/blank",1); + osd_blank("/sys/class/graphics/fb1/blank",0); + + codecParam.stream_type = STREAM_TYPE_ES_VIDEO; + codecParam.has_video = 1; + codecParam.noblock = 0; + + switch (videoFormat) { + case VIDEO_FORMAT_H264: + if (width > 1920 || height > 1080) { + codecParam.video_type = VFORMAT_H264_4K2K; + codecParam.am_sysinfo.format = VIDEO_DEC_FORMAT_H264_4K2K; + } else { + codecParam.video_type = VFORMAT_H264; + codecParam.am_sysinfo.format = VIDEO_DEC_FORMAT_H264; + } + break; + case VIDEO_FORMAT_H265: + codecParam.video_type = VFORMAT_HEVC; + codecParam.am_sysinfo.format = VIDEO_DEC_FORMAT_HEVC; + break; + default: + printf("Video format not supported\n"); + exit(1); + } + + codecParam.am_sysinfo.width = width; + codecParam.am_sysinfo.height = height; + codecParam.am_sysinfo.rate = 96000 / redrawRate; + codecParam.am_sysinfo.param = (void *)(SYNC_OUTSIDE); + + int api = codec_init(&codecParam); + if (api != 0) { + fprintf(stderr, "codec_init error: %x\n", api); + exit(1); + } +} + +void aml_cleanup() { + int api = codec_close(&codecParam); + osd_blank("/sys/class/graphics/fb0/blank",0); + osd_blank("/sys/class/graphics/fb1/blank",0); +} + +int aml_submit_decode_unit(PDECODE_UNIT decodeUnit) { + int result = DR_OK; + PLENTRY entry = decodeUnit->bufferList; + while (entry != NULL) { + int api = codec_write(&codecParam, entry->data, entry->length); + if (api != entry->length) { + fprintf(stderr, "codec_write error: %x\n", api); + codec_reset(&codecParam); + result = DR_NEED_IDR; + break; + } + + entry = entry->next; + } + return result; +} + +DECODER_RENDERER_CALLBACKS decoder_callbacks_aml = { + .setup = aml_setup, + .cleanup = aml_cleanup, + .submitDecodeUnit = aml_submit_decode_unit, + .capabilities = CAPABILITY_DIRECT_SUBMIT | CAPABILITY_SLICES_PER_FRAME(8), +}; diff --git a/src/video/fake.c b/src/video/fake.c index 6468ac6..a9e3e3d 100644 --- a/src/video/fake.c +++ b/src/video/fake.c @@ -17,14 +17,14 @@ * along with Moonlight; if not, see . */ -#include "limelight-common/Limelight.h" +#include #include static FILE* fd; static const char* fileName = "fake.h264"; -void decoder_renderer_setup(int width, int height, int redrawRate, void* context, int drFlags) { +void decoder_renderer_setup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) { fd = fopen(fileName, "w"); } diff --git a/src/video/ffmpeg.c b/src/video/ffmpeg.c index 9380f6a..7de31e5 100644 --- a/src/video/ffmpeg.c +++ b/src/video/ffmpeg.c @@ -23,6 +23,8 @@ #include "ffmpeg_vdpau.h" #endif +#include + #include #include #include @@ -42,7 +44,7 @@ enum decoders decoder_system; // This function must be called before // any other decoding functions -int ffmpeg_init(int width, int height, int perf_lvl, int thread_count) { +int ffmpeg_init(int videoFormat, int width, int height, int perf_lvl, int thread_count) { // Initialize the avcodec library and register codecs av_log_set_level(AV_LOG_QUIET); avcodec_register_all(); @@ -51,7 +53,15 @@ int ffmpeg_init(int width, int height, int perf_lvl, int thread_count) { #ifdef HAVE_VDPAU if (perf_lvl & HARDWARE_ACCELERATION) { - decoder = avcodec_find_decoder_by_name("h264_vdpau"); + switch (videoFormat) { + case VIDEO_FORMAT_H264: + decoder = avcodec_find_decoder_by_name("h264_vdpau"); + break; + case VIDEO_FORMAT_H265: + decoder = avcodec_find_decoder_by_name("hevc_vdpau"); + break; + } + if (decoder != NULL) decoder_system = VDPAU; } @@ -59,7 +69,14 @@ int ffmpeg_init(int width, int height, int perf_lvl, int thread_count) { if (decoder == NULL) { decoder_system = SOFTWARE; - decoder = avcodec_find_decoder_by_name("h264"); + switch (videoFormat) { + case VIDEO_FORMAT_H264: + decoder = avcodec_find_decoder_by_name("h264"); + break; + case VIDEO_FORMAT_H265: + decoder = avcodec_find_decoder_by_name("hevc"); + break; + } if (decoder == NULL) { printf("Couldn't find decoder\n"); return -1; @@ -89,7 +106,7 @@ int ffmpeg_init(int width, int height, int perf_lvl, int thread_count) { decoder_ctx->width = width; decoder_ctx->height = height; - decoder_ctx->pix_fmt = PIX_FMT_YUV420P; + decoder_ctx->pix_fmt = AV_PIX_FMT_YUV420P; int err = avcodec_open2(decoder_ctx, decoder, NULL); if (err < 0) { diff --git a/src/video/ffmpeg.h b/src/video/ffmpeg.h index 1421e2d..2714222 100644 --- a/src/video/ffmpeg.h +++ b/src/video/ffmpeg.h @@ -34,7 +34,7 @@ // Uses hardware acceleration #define HARDWARE_ACCELERATION 0x40 -int ffmpeg_init(int width, int height, int perf_lvl, int thread_count); +int ffmpeg_init(int videoFormat, int width, int height, int perf_lvl, int thread_count); void ffmpeg_destroy(void); int ffmpeg_draw_frame(AVFrame *pict); diff --git a/src/video/ffmpeg_vdpau.c b/src/video/ffmpeg_vdpau.c index c8c9cc2..ae2b119 100644 --- a/src/video/ffmpeg_vdpau.c +++ b/src/video/ffmpeg_vdpau.c @@ -64,23 +64,22 @@ struct vdpau_render_state* vdp_get_free_render_state() { return render_state; } -static int vdp_get_buffer(AVCodecContext* context, AVFrame* frame) { +static void vdp_release_buffer(void* opaque, uint8_t *data) { + struct vdpau_render_state *render_state = (struct vdpau_render_state *) data; + render_state->state = 0; +} + +static int vdp_get_buffer(AVCodecContext* context, AVFrame* frame, int flags) { struct vdpau_render_state* pRenderState = vdp_get_free_render_state(); frame->data[0] = (uint8_t*) pRenderState; - frame->type = FF_BUFFER_TYPE_USER; + frame->buf[0] = av_buffer_create(frame->data[0], 0, vdp_release_buffer, NULL, 0); pRenderState->state |= FF_VDPAU_STATE_USED_FOR_RENDER; return 0; } -static void vdp_release_buffer(AVCodecContext* context, AVFrame* frame) { - struct vdpau_render_state *render_state = (struct vdpau_render_state *)frame->data[0]; - render_state->state = 0; - frame->data[0] = 0; -} - static enum AVPixelFormat vdp_get_format(AVCodecContext* context, const enum AVPixelFormat* pixel_format) { - return PIX_FMT_VDPAU_H264; + return AV_PIX_FMT_VDPAU_H264; } static void vdp_draw_horiz_band(struct AVCodecContext* context, const AVFrame* frame, int offset[4], int y, int type, int height) { @@ -108,8 +107,7 @@ int vdpau_init(AVCodecContext* decoder_ctx, int width, int height) { vdp_get_proc_address(vdp_device, VDP_FUNC_ID_DECODER_CREATE, (void**)&vdp_decoder_create); vdp_get_proc_address(vdp_device, VDP_FUNC_ID_VIDEO_MIXER_CREATE, (void**)&vdp_video_mixer_create); - decoder_ctx->get_buffer = vdp_get_buffer; - decoder_ctx->release_buffer = vdp_release_buffer; + decoder_ctx->get_buffer2 = vdp_get_buffer; decoder_ctx->draw_horiz_band = vdp_draw_horiz_band; decoder_ctx->get_format = vdp_get_format; decoder_ctx->slice_flags = SLICE_FLAG_CODED_ORDER | SLICE_FLAG_ALLOW_FIELD; @@ -119,7 +117,7 @@ int vdpau_init(AVCodecContext* decoder_ctx, int width, int height) { printf("Couldn't allocate frame\n"); return -1; } - cpu_frame->format = PIX_FMT_YUV420P; + cpu_frame->format = AV_PIX_FMT_YUV420P; cpu_frame->width = width; cpu_frame->height = height; av_frame_get_buffer(cpu_frame, 32); diff --git a/src/video/imx.c b/src/video/imx.c index 740b566..2cf4daa 100644 --- a/src/video/imx.c +++ b/src/video/imx.c @@ -17,7 +17,7 @@ * along with Moonlight; if not, see . */ -#include "limelight-common/Limelight.h" +#include #include #include @@ -83,7 +83,12 @@ bool video_imx_init() { return vpu_Init(NULL) == RETCODE_SUCCESS; } -static void decoder_renderer_setup(int width, int height, int redrawRate, void* context, int drFlags) { +static void decoder_renderer_setup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) { + if (videoFormat != VIDEO_FORMAT_H264) { + fprintf(stderr, "Video format not supported\n"); + exit(1); + } + struct mxcfb_gbl_alpha alpha; dbuf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT; diff --git a/src/video/pi.c b/src/video/pi.c index 9ff5eb7..c433e08 100644 --- a/src/video/pi.c +++ b/src/video/pi.c @@ -30,7 +30,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "sps.h" -#include "limelight-common/Limelight.h" +#include #include #include @@ -53,7 +53,12 @@ static unsigned char *dest; static int port_settings_changed; static int first_packet; -static void decoder_renderer_setup(int width, int height, int redrawRate, void* context, int drFlags) { +static void decoder_renderer_setup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) { + if (videoFormat != VIDEO_FORMAT_H264) { + fprintf(stderr, "Video format not supported\n"); + exit(1); + } + bcm_host_init(); gs_sps_init(width, height); diff --git a/src/video/sdl.c b/src/video/sdl.c index 89cb657..5a9734f 100644 --- a/src/video/sdl.c +++ b/src/video/sdl.c @@ -21,7 +21,7 @@ #include "../sdl.h" #include "ffmpeg.h" -#include "limelight-common/Limelight.h" +#include #include #include @@ -32,9 +32,12 @@ static char* ffmpeg_buffer; -static void sdl_setup(int width, int height, int redrawRate, void* context, int drFlags) { +static void sdl_setup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) { int avc_flags = SLICE_THREADING; - if (ffmpeg_init(width, height, avc_flags, 2) < 0) { + if (drFlags & FORCE_HARDWARE_ACCELERATION) + avc_flags |= HARDWARE_ACCELERATION; + + if (ffmpeg_init(videoFormat, width, height, avc_flags, 2) < 0) { fprintf(stderr, "Couldn't initialize video decoding\n"); exit(1); } diff --git a/third_party/moonlight-common-c b/third_party/moonlight-common-c index fbd58c6..a6d9ab0 160000 --- a/third_party/moonlight-common-c +++ b/third_party/moonlight-common-c @@ -1 +1 @@ -Subproject commit fbd58c60ea12d8760bae86b7d98b2d0208418baf +Subproject commit a6d9ab0664c6e5229674b84f2591be4d04c79239