From 7c79fcc6a29fa1366a01a2abbc6b7a9b66a69d5c Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 26 Sep 2026 15:38:07 -0500 Subject: [PATCH 01/13] Raise deployment target for Xcode 27 --- Moonlight.xcodeproj/project.pbxproj | 8 ++++---- .../moonlight-common.xcodeproj/project.pbxproj | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Moonlight.xcodeproj/project.pbxproj b/Moonlight.xcodeproj/project.pbxproj index 6f05557..0fc45fd 100644 --- a/Moonlight.xcodeproj/project.pbxproj +++ b/Moonlight.xcodeproj/project.pbxproj @@ -1022,7 +1022,7 @@ SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; - TVOS_DEPLOYMENT_TARGET = 12.0; + TVOS_DEPLOYMENT_TARGET = 15.0; }; name = Debug; }; @@ -1078,7 +1078,7 @@ SWIFT_OBJC_INTERFACE_HEADER_NAME = "Moonlight-Swift.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; - TVOS_DEPLOYMENT_TARGET = 12.0; + TVOS_DEPLOYMENT_TARGET = 15.0; }; name = Release; }; @@ -1129,7 +1129,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_OBJC_INTERFACE_HEADER_NAME = ""; @@ -1182,7 +1182,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; PROVISIONING_PROFILE = ""; SDKROOT = iphoneos; SWIFT_OBJC_INTERFACE_HEADER_NAME = ""; diff --git a/moonlight-common/moonlight-common.xcodeproj/project.pbxproj b/moonlight-common/moonlight-common.xcodeproj/project.pbxproj index 8eed6eb..4de2c5f 100644 --- a/moonlight-common/moonlight-common.xcodeproj/project.pbxproj +++ b/moonlight-common/moonlight-common.xcodeproj/project.pbxproj @@ -510,7 +510,7 @@ SDKROOT = appletvos; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = 3; - TVOS_DEPLOYMENT_TARGET = 12.0; + TVOS_DEPLOYMENT_TARGET = 15.0; VALIDATE_PRODUCT = NO; }; name = Debug; @@ -540,7 +540,7 @@ SDKROOT = appletvos; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = 3; - TVOS_DEPLOYMENT_TARGET = 12.0; + TVOS_DEPLOYMENT_TARGET = 15.0; VALIDATE_PRODUCT = NO; }; name = Release; @@ -678,7 +678,7 @@ "moonlight-common-c/enet/include", "../libs/**", ); - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; ONLY_ACTIVE_ARCH = NO; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -696,7 +696,7 @@ "moonlight-common-c/enet/include", "../libs/**", ); - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; ONLY_ACTIVE_ARCH = NO; PRODUCT_NAME = "$(TARGET_NAME)"; From 938f4b45894ecb5576f7e4dab9ade1679ae761cc Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 26 Sep 2026 15:45:47 -0500 Subject: [PATCH 02/13] Update moonlight-common-c --- Limelight/Stream/VideoDecoderRenderer.m | 2 +- moonlight-common/moonlight-common-c | 2 +- .../project.pbxproj | 46 +++++++++++++------ 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/Limelight/Stream/VideoDecoderRenderer.m b/Limelight/Stream/VideoDecoderRenderer.m index a2adf08..5c89e5a 100644 --- a/Limelight/Stream/VideoDecoderRenderer.m +++ b/Limelight/Stream/VideoDecoderRenderer.m @@ -580,7 +580,7 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit); CMSampleBufferRef sampleBuffer; - CMSampleTimingInfo sampleTiming = {kCMTimeInvalid, CMTimeMake(du->presentationTimeMs, 1000), kCMTimeInvalid}; + CMSampleTimingInfo sampleTiming = {kCMTimeInvalid, CMTimeMake(du->presentationTimeUs, 1000000), kCMTimeInvalid}; status = CMSampleBufferCreateReady(kCFAllocatorDefault, frameBlockBuffer, diff --git a/moonlight-common/moonlight-common-c b/moonlight-common/moonlight-common-c index 48d7f1a..f900dd4 160000 --- a/moonlight-common/moonlight-common-c +++ b/moonlight-common/moonlight-common-c @@ -1 +1 @@ -Subproject commit 48d7f1ace168d07dad339718f1350979545f15d2 +Subproject commit f900dd4767759c7b9d0e93bcea666b55c69ea62f diff --git a/moonlight-common/moonlight-common.xcodeproj/project.pbxproj b/moonlight-common/moonlight-common.xcodeproj/project.pbxproj index 4de2c5f..dadda71 100644 --- a/moonlight-common/moonlight-common.xcodeproj/project.pbxproj +++ b/moonlight-common/moonlight-common.xcodeproj/project.pbxproj @@ -9,7 +9,12 @@ /* Begin PBXBuildFile section */ 983091DE24CEB69B00A0835D /* ConnectionTester.c in Sources */ = {isa = PBXBuildFile; fileRef = 983091DD24CEB69B00A0835D /* ConnectionTester.c */; }; 983091DF24CEB69B00A0835D /* ConnectionTester.c in Sources */ = {isa = PBXBuildFile; fileRef = 983091DD24CEB69B00A0835D /* ConnectionTester.c */; }; - 9857AEF01EBE85E10084F99E /* rs.c in Sources */ = {isa = PBXBuildFile; fileRef = 9857AEEE1EBE85E10084F99E /* rs.c */; }; + 9857AF051EBE85E10084F99E /* rs.c in Sources */ = {isa = PBXBuildFile; fileRef = 9857AF011EBE85E10084F99E /* rs.c */; }; + 9857AF061EBE85E10084F99E /* oblas_common.c in Sources */ = {isa = PBXBuildFile; fileRef = 9857AF031EBE85E10084F99E /* oblas_common.c */; }; + 9857AF071EBE85E10084F99E /* oblas_lite.c in Sources */ = {isa = PBXBuildFile; fileRef = 9857AF041EBE85E10084F99E /* oblas_lite.c */; }; + 9857AF081EBE85E10084F99E /* rs.c in Sources */ = {isa = PBXBuildFile; fileRef = 9857AF011EBE85E10084F99E /* rs.c */; }; + 9857AF091EBE85E10084F99E /* oblas_common.c in Sources */ = {isa = PBXBuildFile; fileRef = 9857AF031EBE85E10084F99E /* oblas_common.c */; }; + 9857AF0A1EBE85E10084F99E /* oblas_lite.c in Sources */ = {isa = PBXBuildFile; fileRef = 9857AF041EBE85E10084F99E /* oblas_lite.c */; }; 98AB2E401CAD425A0089BB98 /* AudioStream.c in Sources */ = {isa = PBXBuildFile; fileRef = 98AB2E1A1CAD423B0089BB98 /* AudioStream.c */; }; 98AB2E411CAD425A0089BB98 /* ByteBuffer.c in Sources */ = {isa = PBXBuildFile; fileRef = 98AB2E1B1CAD423B0089BB98 /* ByteBuffer.c */; }; 98AB2E421CAD425A0089BB98 /* Connection.c in Sources */ = {isa = PBXBuildFile; fileRef = 98AB2E1D1CAD423B0089BB98 /* Connection.c */; }; @@ -44,7 +49,6 @@ 98F145382667D847004018E3 /* RtpVideoQueue.c in Sources */ = {isa = PBXBuildFile; fileRef = 98F145342667D847004018E3 /* RtpVideoQueue.c */; }; F74BEF9F2C1A707200224667 /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = F74BEF9E2C1A707200224667 /* OpenSSL */; }; F74BEFA22C1A80D000224667 /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = F74BEFA12C1A80D000224667 /* OpenSSL */; }; - FB1A67F821324F4900507771 /* rs.c in Sources */ = {isa = PBXBuildFile; fileRef = 9857AEEE1EBE85E10084F99E /* rs.c */; }; FB1A67FA21324F4900507771 /* AudioStream.c in Sources */ = {isa = PBXBuildFile; fileRef = 98AB2E1A1CAD423B0089BB98 /* AudioStream.c */; }; FB1A67FB21324F4900507771 /* ByteBuffer.c in Sources */ = {isa = PBXBuildFile; fileRef = 98AB2E1B1CAD423B0089BB98 /* ByteBuffer.c */; }; FB1A67FC21324F4900507771 /* Connection.c in Sources */ = {isa = PBXBuildFile; fileRef = 98AB2E1D1CAD423B0089BB98 /* Connection.c */; }; @@ -85,8 +89,10 @@ /* Begin PBXFileReference section */ 983091DD24CEB69B00A0835D /* ConnectionTester.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = ConnectionTester.c; sourceTree = ""; }; - 9857AEEE1EBE85E10084F99E /* rs.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = rs.c; path = "moonlight-common-c/reedsolomon/rs.c"; sourceTree = ""; }; - 9857AEEF1EBE85E10084F99E /* rs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = rs.h; path = "moonlight-common-c/reedsolomon/rs.h"; sourceTree = ""; }; + 9857AF011EBE85E10084F99E /* rs.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = rs.c; path = "moonlight-common-c/nanors/rs.c"; sourceTree = ""; }; + 9857AF021EBE85E10084F99E /* rs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = rs.h; path = "moonlight-common-c/nanors/rs.h"; sourceTree = ""; }; + 9857AF031EBE85E10084F99E /* oblas_common.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = oblas_common.c; path = "moonlight-common-c/nanors/deps/obl/oblas_common.c"; sourceTree = ""; }; + 9857AF041EBE85E10084F99E /* oblas_lite.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = oblas_lite.c; path = "moonlight-common-c/nanors/deps/obl/oblas_lite.c"; sourceTree = ""; }; 98AB2DF01CAD422B0089BB98 /* callbacks.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = callbacks.c; sourceTree = ""; }; 98AB2DF11CAD422B0089BB98 /* ChangeLog */ = {isa = PBXFileReference; lastKnownFileType = text; path = ChangeLog; sourceTree = ""; }; 98AB2DF21CAD422B0089BB98 /* CMakeLists.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CMakeLists.txt; sourceTree = ""; }; @@ -180,13 +186,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 9857AEED1EBE85D50084F99E /* reedsolomon */ = { + 9857AF001EBE85D50084F99E /* nanors */ = { isa = PBXGroup; children = ( - 9857AEEE1EBE85E10084F99E /* rs.c */, - 9857AEEF1EBE85E10084F99E /* rs.h */, + 9857AF011EBE85E10084F99E /* rs.c */, + 9857AF021EBE85E10084F99E /* rs.h */, + 9857AF031EBE85E10084F99E /* oblas_common.c */, + 9857AF041EBE85E10084F99E /* oblas_lite.c */, ); - name = reedsolomon; + name = nanors; sourceTree = ""; }; 98AB2DEF1CAD422B0089BB98 /* enet */ = { @@ -310,7 +318,7 @@ FB290E2519B37A4E004C83CF = { isa = PBXGroup; children = ( - 9857AEED1EBE85D50084F99E /* reedsolomon */, + 9857AF001EBE85D50084F99E /* nanors */, 98AB2E191CAD423B0089BB98 /* src */, 98AB2DEF1CAD422B0089BB98 /* enet */, FB290E2F19B37A4E004C83CF /* Products */, @@ -411,7 +419,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - FB1A67F821324F4900507771 /* rs.c in Sources */, + 9857AF081EBE85E10084F99E /* rs.c in Sources */, + 9857AF091EBE85E10084F99E /* oblas_common.c in Sources */, + 9857AF0A1EBE85E10084F99E /* oblas_lite.c in Sources */, 98F145362667D847004018E3 /* RtpAudioQueue.c in Sources */, FB1A67FA21324F4900507771 /* AudioStream.c in Sources */, FB1A67FB21324F4900507771 /* ByteBuffer.c in Sources */, @@ -448,6 +458,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9857AF051EBE85E10084F99E /* rs.c in Sources */, + 9857AF061EBE85E10084F99E /* oblas_common.c in Sources */, + 9857AF071EBE85E10084F99E /* oblas_lite.c in Sources */, 98C1FC142184124C0023FDDF /* SimpleStun.c in Sources */, 98F145352667D847004018E3 /* RtpAudioQueue.c in Sources */, 98AB2E401CAD425A0089BB98 /* AudioStream.c in Sources */, @@ -475,7 +488,6 @@ 98AB2E541CAD427A0089BB98 /* packet.c in Sources */, 98AB2E551CAD427A0089BB98 /* peer.c in Sources */, 98AB2E561CAD427A0089BB98 /* protocol.c in Sources */, - 9857AEF01EBE85E10084F99E /* rs.c in Sources */, 98AB2E571CAD427A0089BB98 /* unix.c in Sources */, 98AB2E581CAD427A0089BB98 /* win32.c in Sources */, ); @@ -500,7 +512,8 @@ GCC_C_LANGUAGE_STANDARD = gnu11; HEADER_SEARCH_PATHS = ( "moonlight-common-c/enet/include", - "../libs/**", + "moonlight-common-c/nanors", + "moonlight-common-c/nanors/deps/obl", ); MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; MTL_ENABLE_DEBUG_INFO = YES; @@ -530,7 +543,8 @@ GCC_C_LANGUAGE_STANDARD = gnu11; HEADER_SEARCH_PATHS = ( "moonlight-common-c/enet/include", - "../libs/**", + "moonlight-common-c/nanors", + "moonlight-common-c/nanors/deps/obl", ); MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; MTL_ENABLE_DEBUG_INFO = NO; @@ -676,7 +690,8 @@ EXECUTABLE_PREFIX = lib; HEADER_SEARCH_PATHS = ( "moonlight-common-c/enet/include", - "../libs/**", + "moonlight-common-c/nanors", + "moonlight-common-c/nanors/deps/obl", ); IPHONEOS_DEPLOYMENT_TARGET = 15.0; MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; @@ -694,7 +709,8 @@ EXECUTABLE_PREFIX = lib; HEADER_SEARCH_PATHS = ( "moonlight-common-c/enet/include", - "../libs/**", + "moonlight-common-c/nanors", + "moonlight-common-c/nanors/deps/obl", ); IPHONEOS_DEPLOYMENT_TARGET = 15.0; MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; From 5cc4076d94f63c4cd46c9b0724dfcea7c3b50464 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 26 Sep 2026 15:47:11 -0500 Subject: [PATCH 03/13] Update project to Xcode 27 recommended settings --- Moonlight.xcodeproj/project.pbxproj | 21 ++++++++++++------- .../project.pbxproj | 12 ++++++----- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/Moonlight.xcodeproj/project.pbxproj b/Moonlight.xcodeproj/project.pbxproj index 0fc45fd..e686ec1 100644 --- a/Moonlight.xcodeproj/project.pbxproj +++ b/Moonlight.xcodeproj/project.pbxproj @@ -736,8 +736,9 @@ FB290CE619B2C406004C83CF /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 1400; + LastUpgradeCheck = 2700; ORGANIZATIONNAME = "Moonlight Game Streaming Project"; TargetAttributes = { FB1A67522132419700507771 = { @@ -984,7 +985,6 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = DM46QST4M7; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_INCREASE_PRECOMPILED_HEADER_SHARING = NO; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -996,6 +996,7 @@ ); INFOPLIST_FILE = "Moonlight TV/Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games"; + IPHONEOS_DEPLOYMENT_TARGET = "$(RECOMMENDED_IPHONEOS_DEPLOYMENT_TARGET)"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1022,7 +1023,7 @@ SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; - TVOS_DEPLOYMENT_TARGET = 15.0; + TVOS_DEPLOYMENT_TARGET = "$(RECOMMENDED_TVOS_DEPLOYMENT_TARGET)"; }; name = Debug; }; @@ -1040,7 +1041,6 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = DM46QST4M7; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_INCREASE_PRECOMPILED_HEADER_SHARING = NO; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -1052,6 +1052,7 @@ ); INFOPLIST_FILE = "Moonlight TV/Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games"; + IPHONEOS_DEPLOYMENT_TARGET = "$(RECOMMENDED_IPHONEOS_DEPLOYMENT_TARGET)"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1078,7 +1079,7 @@ SWIFT_OBJC_INTERFACE_HEADER_NAME = "Moonlight-Swift.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; - TVOS_DEPLOYMENT_TARGET = 15.0; + TVOS_DEPLOYMENT_TARGET = "$(RECOMMENDED_TVOS_DEPLOYMENT_TARGET)"; }; name = Release; }; @@ -1112,8 +1113,10 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEVELOPMENT_TEAM = DM46QST4M7; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -1132,6 +1135,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.0; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_OBJC_INTERFACE_HEADER_NAME = ""; SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -1168,8 +1172,10 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = YES; + DEVELOPMENT_TEAM = DM46QST4M7; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_PREPROCESSOR_DEFINITIONS = ( @@ -1185,6 +1191,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.0; PROVISIONING_PROFILE = ""; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_OBJC_INTERFACE_HEADER_NAME = ""; SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -1200,7 +1207,6 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = DM46QST4M7; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Limelight/Limelight-Prefix.pch"; HEADER_SEARCH_PATHS = ( @@ -1212,6 +1218,7 @@ ); INFOPLIST_FILE = "Limelight/Limelight-Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games"; + IPHONEOS_DEPLOYMENT_TARGET = "$(RECOMMENDED_IPHONEOS_DEPLOYMENT_TARGET)"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1250,7 +1257,6 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = DM46QST4M7; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Limelight/Limelight-Prefix.pch"; HEADER_SEARCH_PATHS = ( @@ -1262,6 +1268,7 @@ ); INFOPLIST_FILE = "Limelight/Limelight-Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games"; + IPHONEOS_DEPLOYMENT_TARGET = "$(RECOMMENDED_IPHONEOS_DEPLOYMENT_TARGET)"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/moonlight-common/moonlight-common.xcodeproj/project.pbxproj b/moonlight-common/moonlight-common.xcodeproj/project.pbxproj index dadda71..4c56993 100644 --- a/moonlight-common/moonlight-common.xcodeproj/project.pbxproj +++ b/moonlight-common/moonlight-common.xcodeproj/project.pbxproj @@ -383,7 +383,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1510; + LastUpgradeCheck = 2700; ORGANIZATIONNAME = "Moonlight Stream"; TargetAttributes = { FB1A67EE21324EE300507771 = { @@ -523,7 +523,7 @@ SDKROOT = appletvos; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = 3; - TVOS_DEPLOYMENT_TARGET = 15.0; + TVOS_DEPLOYMENT_TARGET = "$(RECOMMENDED_TVOS_DEPLOYMENT_TARGET)"; VALIDATE_PRODUCT = NO; }; name = Debug; @@ -554,7 +554,7 @@ SDKROOT = appletvos; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = 3; - TVOS_DEPLOYMENT_TARGET = 15.0; + TVOS_DEPLOYMENT_TARGET = "$(RECOMMENDED_TVOS_DEPLOYMENT_TARGET)"; VALIDATE_PRODUCT = NO; }; name = Release; @@ -620,6 +620,7 @@ ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; }; name = Debug; }; @@ -680,6 +681,7 @@ PROVISIONING_PROFILE = ""; SDKROOT = macosx; SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; }; name = Release; }; @@ -693,7 +695,7 @@ "moonlight-common-c/nanors", "moonlight-common-c/nanors/deps/obl", ); - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = "$(RECOMMENDED_IPHONEOS_DEPLOYMENT_TARGET)"; MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; ONLY_ACTIVE_ARCH = NO; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -712,7 +714,7 @@ "moonlight-common-c/nanors", "moonlight-common-c/nanors/deps/obl", ); - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = "$(RECOMMENDED_IPHONEOS_DEPLOYMENT_TARGET)"; MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; ONLY_ACTIVE_ARCH = NO; PRODUCT_NAME = "$(TARGET_NAME)"; From ca0efbdb5b343d2561cb2e8c515baf85cefd41ac Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 26 Sep 2026 16:00:45 -0500 Subject: [PATCH 04/13] Distinguish between Enter and Numpad Enter --- Limelight/Input/KeyboardSupport.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Limelight/Input/KeyboardSupport.m b/Limelight/Input/KeyboardSupport.m index 8cbf7a3..f4b8f5a 100644 --- a/Limelight/Input/KeyboardSupport.m +++ b/Limelight/Input/KeyboardSupport.m @@ -202,6 +202,7 @@ break; case UIKeyboardHIDUsageKeypadEnter: keyCode = 0x0D; + modifierFlags |= MODIFIER_EXTENDED; break; case UIKeyboardHIDUsageKeypadPeriod: keyCode = 0x6E; From 064723092bc8d9329117a4a97c2428dc056de4ba Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 26 Sep 2026 16:01:17 -0500 Subject: [PATCH 05/13] Stringify YUV 4:4:4 video formats --- Limelight/Stream/Connection.m | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Limelight/Stream/Connection.m b/Limelight/Stream/Connection.m index 8ce40b6..33361fc 100644 --- a/Limelight/Stream/Connection.m +++ b/Limelight/Stream/Connection.m @@ -86,8 +86,12 @@ void DrStop(void) { case VIDEO_FORMAT_H264: return @"H.264"; + case VIDEO_FORMAT_H264_HIGH8_444: + return @"H.264 (4:4:4)"; case VIDEO_FORMAT_H265: return @"HEVC"; + case VIDEO_FORMAT_H265_REXT8_444: + return @"HEVC (4:4:4)"; case VIDEO_FORMAT_H265_MAIN10: if (LiGetCurrentHostDisplayHdrMode()) { return @"HEVC Main 10 HDR"; @@ -95,8 +99,17 @@ void DrStop(void) else { return @"HEVC Main 10 SDR"; } + case VIDEO_FORMAT_H265_REXT10_444: + if (LiGetCurrentHostDisplayHdrMode()) { + return @"HEVC Main 10 HDR (4:4:4)"; + } + else { + return @"HEVC Main 10 SDR (4:4:4)"; + } case VIDEO_FORMAT_AV1_MAIN8: return @"AV1"; + case VIDEO_FORMAT_AV1_HIGH8_444: + return @"AV1 (4:4:4)"; case VIDEO_FORMAT_AV1_MAIN10: if (LiGetCurrentHostDisplayHdrMode()) { return @"AV1 10-bit HDR"; @@ -104,6 +117,13 @@ void DrStop(void) else { return @"AV1 10-bit SDR"; } + case VIDEO_FORMAT_AV1_HIGH10_444: + if (LiGetCurrentHostDisplayHdrMode()) { + return @"AV1 10-bit HDR (4:4:4)"; + } + else { + return @"AV1 10-bit SDR (4:4:4)"; + } default: return @"UNKNOWN"; } From 02dc9780496eeeac6d01c8bbdccb8b6fe71ef28a Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 26 Sep 2026 16:06:48 -0500 Subject: [PATCH 06/13] Switch to GitHub Actions --- .github/workflows/ci.yml | 45 ++++++++++++++++++++++++++++++++++++++++ README.md | 2 +- appveyor.yml | 24 --------------------- 3 files changed, 46 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 appveyor.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4c75fde --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,45 @@ +name: CI + +on: + push: + pull_request: + branches: + - master + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build: + name: Build (${{ matrix.platform }}) + runs-on: macos-latest + strategy: + fail-fast: false + matrix: + include: + - scheme: Moonlight + sdk: iphonesimulator + platform: iOS + - scheme: Moonlight TV + sdk: appletvsimulator + platform: tvOS + + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + submodules: recursive + + - name: Download ${{ matrix.platform }} platform + run: xcodebuild -downloadPlatform ${{ matrix.platform }} + + - name: Build Debug + run: xcodebuild -project Moonlight.xcodeproj -scheme "${{ matrix.scheme }}" -configuration Debug -sdk ${{ matrix.sdk }} clean build + + - name: Build Release + run: xcodebuild -project Moonlight.xcodeproj -scheme "${{ matrix.scheme }}" -configuration Release -sdk ${{ matrix.sdk }} clean build diff --git a/README.md b/README.md index 6580a88..4b2cf99 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Moonlight iOS/tvOS -[![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/kwv8vpwr457lqn25/branch/master?svg=true)](https://ci.appveyor.com/project/cgutman/moonlight-ios/branch/master) +[![CI](https://github.com/moonlight-stream/moonlight-ios/actions/workflows/ci.yml/badge.svg)](https://github.com/moonlight-stream/moonlight-ios/actions/workflows/ci.yml) [Moonlight for iOS/tvOS](https://moonlight-stream.org) is an open source client for [Sunshine](https://github.com/LizardByte/Sunshine) and NVIDIA GameStream. Moonlight for iOS/tvOS allows you to stream your full collection of games and apps from your powerful desktop computer to your iOS device or Apple TV. diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 91cab2d..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,24 +0,0 @@ -version: 0.0.0.{build} - -clone_depth: 1 - -environment: - matrix: - - APPVEYOR_BUILD_WORKER_IMAGE: macOS-Sonoma - SCHEME: Moonlight - SDK: iphonesimulator - PLATFORM: iOS - - APPVEYOR_BUILD_WORKER_IMAGE: macOS-Sonoma - SCHEME: Moonlight TV - SDK: appletvsimulator - PLATFORM: tvOS - -before_build: - - 'git submodule update --init --recursive' - - sh: 'xcodebuild -downloadPlatform $PLATFORM' - -build_script: - - sh: xcodebuild -project Moonlight.xcodeproj -scheme "$SCHEME" -configuration Debug -sdk $SDK clean build - - sh: xcodebuild -project Moonlight.xcodeproj -scheme "$SCHEME" -configuration Release -sdk $SDK clean build - -deploy: off From 91da39469a1d6f2e62dfe8e3ccbc693aaf842c03 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 29 Oct 2026 18:46:44 +0100 Subject: [PATCH 07/13] Add GameCult iOS 14 TrollStore IPA build Unsigned device build with Xcode 16 on upstream 159ffdb (deployment target iOS 12), before the iOS 15 raise in 7c79fcc. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/gamecult-ios14-ipa.yml | 54 ++++++++++++++++++++++++ README.md | 13 ++++++ 2 files changed, 67 insertions(+) create mode 100644 .github/workflows/gamecult-ios14-ipa.yml diff --git a/.github/workflows/gamecult-ios14-ipa.yml b/.github/workflows/gamecult-ios14-ipa.yml new file mode 100644 index 0000000..6d16091 --- /dev/null +++ b/.github/workflows/gamecult-ios14-ipa.yml @@ -0,0 +1,54 @@ +name: GameCult iOS 14 IPA + +# Builds an unsigned device IPA for TrollStore on iOS/iPadOS 14. +# Upstream raised the deployment target to iOS 15 in 7c79fcc ("for Xcode 27"); +# this branch sits on its parent 159ffdb (target iOS 12) and builds with Xcode 16. + +on: + push: + branches: + - gamecult/ios14-build + workflow_dispatch: + +permissions: + contents: read + +jobs: + ipa: + runs-on: macos-15 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Select Xcode 16 + run: | + ls -d /Applications/Xcode_16*.app + XC=$(ls -d /Applications/Xcode_16*.app | sort -V | tail -1) + sudo xcode-select -s "$XC/Contents/Developer" + xcodebuild -version + + - name: Build unsigned device app + run: | + xcodebuild -project Moonlight.xcodeproj -scheme Moonlight \ + -configuration Release -sdk iphoneos \ + -destination 'generic/platform=iOS' \ + -derivedDataPath build \ + CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" \ + build + + - name: Package IPA + run: | + APP=$(ls -d build/Build/Products/Release-iphoneos/*.app | head -1) + /usr/libexec/PlistBuddy -c 'Print :MinimumOSVersion' "$APP/Info.plist" + mkdir Payload + cp -R "$APP" Payload/ + zip -qry Moonlight-ios14-unsigned.ipa Payload + shasum -a 256 Moonlight-ios14-unsigned.ipa + + - name: Upload IPA + uses: actions/upload-artifact@v4 + with: + name: Moonlight-ios14-unsigned + path: Moonlight-ios14-unsigned.ipa diff --git a/README.md b/README.md index 6580a88..2a4e138 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,16 @@ +# GameCult build of Moonlight iOS for iOS/iPadOS 14 + +This fork exists for one artifact: an **unsigned Moonlight IPA that installs with TrollStore on iOS/iPadOS 14**, for a device that cannot use the App Store. + +- **Branch:** `gamecult/ios14-build`, based on upstream commit `159ffdb` (2026-09-26). That is the last commit before upstream raised the minimum to iOS 15 in `7c79fcc`, a change made for Xcode 27. +- **Build:** `.github/workflows/gamecult-ios14-ipa.yml` runs on GitHub's macOS runner with Xcode 16 and uploads `Moonlight-ios14-unsigned.ipa` as a workflow artifact. +- **Install:** with TrollStore, which signs the app itself. +- **Status:** a personal build, not a maintained distribution. For a supported Moonlight, use the [upstream app](https://github.com/moonlight-stream/moonlight-ios). + +Everything below is upstream's README. + +--- + # Moonlight iOS/tvOS [![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/kwv8vpwr457lqn25/branch/master?svg=true)](https://ci.appveyor.com/project/cgutman/moonlight-ios/branch/master) From 19889385d10d93efc24315c3cd405a75a332c579 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 29 Oct 2026 20:39:41 +0100 Subject: [PATCH 08/13] Read the Xbox One S BLE controller over IOHID on iOS 14 GameController on iOS 14.4 never exposes the Xbox One S controller on Microsoft's Bluetooth LE firmware (VID 0x045E, PID 0x0B20) as a GCController, though iOS connects it as a HID device. ControllerSupport now matches it with IOHIDManager, parses input report 1 (layout as in SDL's HIDAPI Xbox One driver) into the existing controller update path, and drives rumble through output report 3. GCController handling is unchanged; the IOHID source skips the pad when GameController already exposes an Xbox gamepad. Player slot allocation and removal move out of the GCController-specific code so both sources share them. Arrival events now use the Moonlight controller's player index rather than the GCController's. Opening IOHIDLibUserClient needs com.apple.security.iokit-user-client-class; the IPA workflow embeds it with an ad-hoc signature for TrollStore. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/gamecult-ios14-ipa.yml | 5 + GameCult/TrollStore.entitlements | 13 + Limelight/Input/Controller.h | 3 + Limelight/Input/ControllerSupport.m | 363 +++++++++++++++++++---- 4 files changed, 326 insertions(+), 58 deletions(-) create mode 100644 GameCult/TrollStore.entitlements diff --git a/.github/workflows/gamecult-ios14-ipa.yml b/.github/workflows/gamecult-ios14-ipa.yml index 6d16091..a55301a 100644 --- a/.github/workflows/gamecult-ios14-ipa.yml +++ b/.github/workflows/gamecult-ios14-ipa.yml @@ -42,6 +42,11 @@ jobs: run: | APP=$(ls -d build/Build/Products/Release-iphoneos/*.app | head -1) /usr/libexec/PlistBuddy -c 'Print :MinimumOSVersion' "$APP/Info.plist" + # Embed the entitlements with an ad-hoc signature on the main binary; + # TrollStore keeps them when it signs the app on install. + EXE=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$APP/Info.plist") + codesign -f -s - --entitlements GameCult/TrollStore.entitlements "$APP/$EXE" + codesign -d --entitlements - "$APP/$EXE" mkdir Payload cp -R "$APP" Payload/ zip -qry Moonlight-ios14-unsigned.ipa Payload diff --git a/GameCult/TrollStore.entitlements b/GameCult/TrollStore.entitlements new file mode 100644 index 0000000..f174b9b --- /dev/null +++ b/GameCult/TrollStore.entitlements @@ -0,0 +1,13 @@ + + + + + + com.apple.security.iokit-user-client-class + + IOHIDLibUserClient + + + diff --git a/Limelight/Input/Controller.h b/Limelight/Input/Controller.h index 6e4511c..7a15946 100644 --- a/Limelight/Input/Controller.h +++ b/Limelight/Input/Controller.h @@ -19,6 +19,9 @@ typedef struct { } controller_touch_context_t; @property (nullable, nonatomic, retain) GCController* gamepad; +// IOHIDDeviceRef for a gamepad that GameController doesn't expose (see ControllerSupport.m) +@property (nullable, nonatomic) void* hidDevice; +@property (nullable, nonatomic, retain) NSMutableData* hidReportBuffer; @property (nonatomic) int playerIndex; @property (nonatomic) int lastButtonFlags; @property (nonatomic) int emulatingButtonFlags; diff --git a/Limelight/Input/ControllerSupport.m b/Limelight/Input/ControllerSupport.m index 19d1315..72f628c 100644 --- a/Limelight/Input/ControllerSupport.m +++ b/Limelight/Input/ControllerSupport.m @@ -17,8 +17,45 @@ @import GameController; @import AudioToolbox; +// The iOS SDK ships IOKit without its HID headers, though IOKit exports these +// functions. They are declared here for the IOHID gamepad fallback below. +__asm__(".linker_option \"-framework\", \"IOKit\""); +typedef struct __IOHIDManager* IOHIDManagerRef; +typedef struct __IOHIDDevice* IOHIDDeviceRef; +typedef void (*IOHIDDeviceCallback)(void* context, int32_t result, void* sender, IOHIDDeviceRef device); +typedef void (*IOHIDReportCallback)(void* context, int32_t result, void* sender, uint32_t type, uint32_t reportID, uint8_t* report, CFIndex reportLength); +extern IOHIDManagerRef IOHIDManagerCreate(CFAllocatorRef allocator, uint32_t options); +extern void IOHIDManagerSetDeviceMatching(IOHIDManagerRef manager, CFDictionaryRef matching); +extern void IOHIDManagerRegisterDeviceMatchingCallback(IOHIDManagerRef manager, IOHIDDeviceCallback callback, void* context); +extern void IOHIDManagerRegisterDeviceRemovalCallback(IOHIDManagerRef manager, IOHIDDeviceCallback callback, void* context); +extern void IOHIDManagerScheduleWithRunLoop(IOHIDManagerRef manager, CFRunLoopRef runLoop, CFStringRef runLoopMode); +extern void IOHIDManagerUnscheduleFromRunLoop(IOHIDManagerRef manager, CFRunLoopRef runLoop, CFStringRef runLoopMode); +extern int32_t IOHIDManagerOpen(IOHIDManagerRef manager, uint32_t options); +extern int32_t IOHIDManagerClose(IOHIDManagerRef manager, uint32_t options); +extern int32_t IOHIDDeviceOpen(IOHIDDeviceRef device, uint32_t options); +extern void IOHIDDeviceRegisterInputReportCallback(IOHIDDeviceRef device, uint8_t* report, CFIndex reportLength, IOHIDReportCallback callback, void* context); +extern int32_t IOHIDDeviceSetReport(IOHIDDeviceRef device, uint32_t reportType, CFIndex reportID, const uint8_t* report, CFIndex reportLength); +#define kIOHIDReportTypeOutput 1 + +// Xbox One S controller on Microsoft's Bluetooth LE firmware (5.x). GameController +// on iOS 14.4 never exposes it as a GCController, so it is read over IOHID instead. +// Its input report 1 is 17 bytes: report ID, four 16-bit sticks (0-65535, Y down), +// two 10-bit triggers, a 1-8 hat (0 = released), 15 buttons and a consumer byte. +// Output report 3 drives rumble. The layout matches SDL's HIDAPI Xbox One driver. +#define XBOX_BLE_VID 0x045E +#define XBOX_BLE_PID 0x0B20 +#define XBOX_BLE_INPUT_REPORT_LEN 17 + static const double MOUSE_SPEED_DIVISOR = 1.25; +// Called from the IOHID C callbacks, whose bodies are compiled before these methods +@interface ControllerSupport () +-(Controller*) controllerForHIDDevice:(IOHIDDeviceRef)device; +-(void) handleXboxBLEReport:(Controller*)limeController report:(const uint8_t*)report length:(CFIndex)length; +-(void) addHIDDevice:(IOHIDDeviceRef)device; +-(void) removeController:(Controller*)limeController; +@end + @implementation ControllerSupport { id _controllerConnectObserver; id _controllerDisconnectObserver; @@ -38,6 +75,8 @@ static const double MOUSE_SPEED_DIVISOR = 1.25; OnScreenControls *_osc; Controller *_oscController; + + IOHIDManagerRef _hidManager; #define EMULATING_SELECT 0x1 #define EMULATING_SPECIAL 0x2 @@ -64,7 +103,15 @@ static const double MOUSE_SPEED_DIVISOR = 1.25; // No connected controller for this player return; } - + + if (controller.hidDevice) { + // Output report 3: enable both handle motors, magnitudes 0-100, + // maximum duration, no delay, repeated until the next report. + uint8_t report[] = { 0x03, 0x03, 0x00, 0x00, lowFreqMotor / 655, highFreqMotor / 655, 0xFF, 0x00, 0xEB }; + IOHIDDeviceSetReport((IOHIDDeviceRef)controller.hidDevice, kIOHIDReportTypeOutput, report[0], report, sizeof(report)); + return; + } + [controller.lowFreqMotor setMotorAmplitude:lowFreqMotor]; [controller.highFreqMotor setMotorAmplitude:highFreqMotor]; } @@ -617,11 +664,19 @@ static const double MOUSE_SPEED_DIVISOR = 1.25; LB_FLAG | RB_FLAG | LS_CLK_FLAG | RS_CLK_FLAG | A_FLAG | B_FLAG | X_FLAG | Y_FLAG; } } + else if (limeController.hidDevice) { + // Xbox One S on Bluetooth LE firmware, read over IOHID + type = LI_CTYPE_XBOX; + capabilities = LI_CCAP_RUMBLE; + supportedButtonFlags = + PLAY_FLAG | BACK_FLAG | SPECIAL_FLAG | UP_FLAG | DOWN_FLAG | LEFT_FLAG | RIGHT_FLAG | + LB_FLAG | RB_FLAG | LS_CLK_FLAG | RS_CLK_FLAG | A_FLAG | B_FLAG | X_FLAG | Y_FLAG; + } // Report the new controller to the host // NB: This will fail if the connection hasn't been fully established yet // and we will try again later. - if (LiSendControllerArrivalEvent(controller.playerIndex, + if (LiSendControllerArrivalEvent(limeController.playerIndex, [self getActiveGamepadMask], type, supportedButtonFlags, @@ -916,6 +971,167 @@ static const double MOUSE_SPEED_DIVISOR = 1.25; #endif } +-(Controller*) controllerForHIDDevice:(IOHIDDeviceRef)device +{ + for (Controller* controller in [_controllers allValues]) { + if (controller.hidDevice == device) { + return controller; + } + } + return nil; +} + +-(void) handleXboxBLEReport:(Controller*)limeController report:(const uint8_t*)report length:(CFIndex)length +{ + if (length < XBOX_BLE_INPUT_REPORT_LEN || report[0] != 1) { + return; + } + + uint8_t buttons1 = report[14]; + uint8_t buttons2 = report[15]; + + if (_swapABXYButtons) { + UPDATE_BUTTON_FLAG(limeController, B_FLAG, buttons1 & 0x01); + UPDATE_BUTTON_FLAG(limeController, A_FLAG, buttons1 & 0x02); + UPDATE_BUTTON_FLAG(limeController, Y_FLAG, buttons1 & 0x08); + UPDATE_BUTTON_FLAG(limeController, X_FLAG, buttons1 & 0x10); + } + else { + UPDATE_BUTTON_FLAG(limeController, A_FLAG, buttons1 & 0x01); + UPDATE_BUTTON_FLAG(limeController, B_FLAG, buttons1 & 0x02); + UPDATE_BUTTON_FLAG(limeController, X_FLAG, buttons1 & 0x08); + UPDATE_BUTTON_FLAG(limeController, Y_FLAG, buttons1 & 0x10); + } + UPDATE_BUTTON_FLAG(limeController, LB_FLAG, buttons1 & 0x40); + UPDATE_BUTTON_FLAG(limeController, RB_FLAG, buttons1 & 0x80); + + // The consumer byte carries View as well on some firmware revisions + UPDATE_BUTTON_FLAG(limeController, BACK_FLAG, (buttons2 & 0x04) || (report[16] & 0x01)); + UPDATE_BUTTON_FLAG(limeController, PLAY_FLAG, buttons2 & 0x08); + UPDATE_BUTTON_FLAG(limeController, SPECIAL_FLAG, buttons2 & 0x10); + UPDATE_BUTTON_FLAG(limeController, LS_CLK_FLAG, buttons2 & 0x20); + UPDATE_BUTTON_FLAG(limeController, RS_CLK_FLAG, buttons2 & 0x40); + + // Hat: 1 = up, clockwise through 8 = up-left, 0 = released + uint8_t hat = report[13]; + UPDATE_BUTTON_FLAG(limeController, UP_FLAG, hat == 1 || hat == 2 || hat == 8); + UPDATE_BUTTON_FLAG(limeController, RIGHT_FLAG, hat >= 2 && hat <= 4); + UPDATE_BUTTON_FLAG(limeController, DOWN_FLAG, hat >= 4 && hat <= 6); + UPDATE_BUTTON_FLAG(limeController, LEFT_FLAG, hat >= 6 && hat <= 8); + + // Sticks are unsigned with Y pointing down; Moonlight wants signed with Y pointing up + #define XBOX_BLE_AXIS(offset) ((int)(report[(offset)] | (report[(offset) + 1] << 8)) - 0x8000) + #define XBOX_BLE_AXIS_Y(offset) ((short)MIN(-XBOX_BLE_AXIS(offset), 0x7FFF)) + [self updateLeftStick:limeController x:XBOX_BLE_AXIS(1) y:XBOX_BLE_AXIS_Y(3)]; + [self updateRightStick:limeController x:XBOX_BLE_AXIS(5) y:XBOX_BLE_AXIS_Y(7)]; + #undef XBOX_BLE_AXIS + #undef XBOX_BLE_AXIS_Y + + // Triggers are 10 bits + [self updateTriggers:limeController + left:((report[9] | (report[10] << 8)) & 0x3FF) >> 2 + right:((report[11] | (report[12] << 8)) & 0x3FF) >> 2]; + [self updateFinished:limeController]; +} + +static void hidReportCallback(void* context, int32_t result, void* sender, uint32_t type, uint32_t reportID, uint8_t* report, CFIndex reportLength) +{ + ControllerSupport* me = (__bridge ControllerSupport*)context; + Controller* limeController = [me controllerForHIDDevice:(IOHIDDeviceRef)sender]; + if (limeController) { + [me handleXboxBLEReport:limeController report:report length:reportLength]; + } +} + +static void hidDeviceMatched(void* context, int32_t result, void* sender, IOHIDDeviceRef device) +{ + [(__bridge ControllerSupport*)context addHIDDevice:device]; +} + +static void hidDeviceRemoved(void* context, int32_t result, void* sender, IOHIDDeviceRef device) +{ + ControllerSupport* me = (__bridge ControllerSupport*)context; + Controller* limeController = [me controllerForHIDDevice:device]; + if (limeController) { + Log(LOG_I, @"IOHID Xbox controller disconnected"); + [me removeController:limeController]; + } +} + +-(void) addHIDDevice:(IOHIDDeviceRef)device +{ + // Newer iOS versions expose this controller through GameController + // themselves; don't add a second player for the same gamepad. + if (@available(iOS 14.0, tvOS 14.0, *)) { + for (GCController* controller in [GCController controllers]) { + if ([controller.extendedGamepad isKindOfClass:[GCXboxGamepad class]]) { + Log(LOG_I, @"Ignoring IOHID Xbox controller already exposed by GameController"); + return; + } + } + } + + int32_t err = IOHIDDeviceOpen(device, 0); + if (err != 0) { + Log(LOG_W, @"Unable to open IOHID Xbox controller: 0x%x", err); + return; + } + + Controller* limeController = [self allocateController]; + if (limeController == nil) { + return; + } + limeController.hidDevice = device; + limeController.hidReportBuffer = [NSMutableData dataWithLength:64]; + + // The physical View and Xbox buttons make combo emulation unnecessary + limeController.supportedEmulationFlags = 0; + + IOHIDDeviceRegisterInputReportCallback(device, limeController.hidReportBuffer.mutableBytes, + limeController.hidReportBuffer.length, hidReportCallback, (__bridge void*)self); + Log(LOG_I, @"IOHID Xbox controller connected"); + + [self reportControllerArrival:limeController]; + [self updateAutoOnScreenControlMode]; + [_delegate gamepadPresenceChanged]; +} + +-(void) startHIDGamepads +{ + _hidManager = IOHIDManagerCreate(kCFAllocatorDefault, 0); + if (_hidManager == NULL) { + return; + } + + NSDictionary* matching = @{ @"VendorID": @(XBOX_BLE_VID), @"ProductID": @(XBOX_BLE_PID) }; + IOHIDManagerSetDeviceMatching(_hidManager, (__bridge CFDictionaryRef)matching); + IOHIDManagerRegisterDeviceMatchingCallback(_hidManager, hidDeviceMatched, (__bridge void*)self); + IOHIDManagerRegisterDeviceRemovalCallback(_hidManager, hidDeviceRemoved, (__bridge void*)self); + IOHIDManagerScheduleWithRunLoop(_hidManager, CFRunLoopGetMain(), kCFRunLoopDefaultMode); + + // This needs com.apple.security.iokit-user-client-class to allow IOHIDLibUserClient, + // which only the GameCult TrollStore build carries. Without it, opening fails and + // this source stays empty. + int32_t err = IOHIDManagerOpen(_hidManager, 0); + if (err != 0) { + Log(LOG_I, @"IOHID gamepad fallback unavailable: 0x%x", err); + } +} + +-(void) stopHIDGamepads +{ + if (_hidManager == NULL) { + return; + } + + IOHIDManagerRegisterDeviceMatchingCallback(_hidManager, NULL, NULL); + IOHIDManagerRegisterDeviceRemovalCallback(_hidManager, NULL, NULL); + IOHIDManagerUnscheduleFromRunLoop(_hidManager, CFRunLoopGetMain(), kCFRunLoopDefaultMode); + IOHIDManagerClose(_hidManager, 0); + CFRelease(_hidManager); + _hidManager = NULL; +} + -(void) updateAutoOnScreenControlMode { // Auto on-screen control support may not be enabled @@ -949,7 +1165,14 @@ static const double MOUSE_SPEED_DIVISOR = 1.25; } } } - + + // An IOHID gamepad has every button, so it needs no on-screen help + for (Controller* controller in [_controllers allValues]) { + if (controller.hidDevice) { + level = OnScreenControlsLevelOff; + } + } + // If we didn't find a gamepad present and we have a keyboard or mouse, turn // the on-screen controls off to get the overlays out of the way. if (level == OnScreenControlsLevelFull && [ControllerSupport hasKeyboardOrMouse]) { @@ -971,50 +1194,90 @@ static const double MOUSE_SPEED_DIVISOR = 1.25; [self updateAutoOnScreenControlMode]; } --(Controller*) assignController:(GCController*)controller { +-(Controller*) allocateController { for (int i = 0; i < 4; i++) { if (!(_controllerNumbers & (1 << i))) { _controllerNumbers |= (1 << i); - controller.playerIndex = i; - + Controller* limeController = [[Controller alloc] init]; limeController.playerIndex = i; limeController.supportedEmulationFlags = EMULATING_SPECIAL | EMULATING_SELECT; - limeController.gamepad = controller; // If this is player 0, it shares state with the OSC limeController.mergedWithController = _oscController; _oscController.mergedWithController = limeController; - - if (@available(iOS 13.0, tvOS 13.0, *)) { - if (controller.extendedGamepad != nil && - controller.extendedGamepad.buttonOptions != nil) { - // Disable select button emulation since we have a physical select button - limeController.supportedEmulationFlags &= ~EMULATING_SELECT; - } - } - - if (@available(iOS 14.0, tvOS 14.0, *)) { - if (controller.extendedGamepad != nil && - controller.extendedGamepad.buttonHome != nil) { - // Disable special button emulation since we have a physical special button - limeController.supportedEmulationFlags &= ~EMULATING_SPECIAL; - } - } - - // Prepare controller haptics for use - [self initializeControllerHaptics:limeController]; - [_controllers setObject:limeController forKey:[NSNumber numberWithInteger:controller.playerIndex]]; - + [_controllers setObject:limeController forKey:[NSNumber numberWithInteger:i]]; + Log(LOG_I, @"Assigning controller index: %d", i); return limeController; } } - + return nil; } +-(Controller*) assignController:(GCController*)controller { + Controller* limeController = [self allocateController]; + if (limeController == nil) { + return nil; + } + + controller.playerIndex = limeController.playerIndex; + limeController.gamepad = controller; + + if (@available(iOS 13.0, tvOS 13.0, *)) { + if (controller.extendedGamepad != nil && + controller.extendedGamepad.buttonOptions != nil) { + // Disable select button emulation since we have a physical select button + limeController.supportedEmulationFlags &= ~EMULATING_SELECT; + } + } + + if (@available(iOS 14.0, tvOS 14.0, *)) { + if (controller.extendedGamepad != nil && + controller.extendedGamepad.buttonHome != nil) { + // Disable special button emulation since we have a physical special button + limeController.supportedEmulationFlags &= ~EMULATING_SPECIAL; + } + } + + // Prepare controller haptics for use + [self initializeControllerHaptics:limeController]; + + return limeController; +} + +-(void) removeController:(Controller*)limeController { + _controllerNumbers &= ~(1 << limeController.playerIndex); + Log(LOG_I, @"Unassigning controller index: %d", limeController.playerIndex); + + // Stop haptics on this controller + [self cleanupControllerHaptics:limeController]; + + // Stop motion reports on this controller + [self cleanupControllerMotion:limeController]; + + // Stop battery reports on this controller + [self cleanupControllerBattery:limeController]; + + // Disassociate this controller from any controllers merged with it + if (limeController.mergedWithController) { + assert(limeController.mergedWithController.mergedWithController == limeController); + limeController.mergedWithController.mergedWithController = nil; + } + + // Inform the server of the updated active gamepads before removing this controller + [self updateFinished:limeController]; + [_controllers removeObjectForKey:[NSNumber numberWithInteger:limeController.playerIndex]]; + + // Re-evaluate the on-screen control mode + [self updateAutoOnScreenControlMode]; + + // Notify the delegate + [_delegate gamepadPresenceChanged]; +} + -(Controller*) getOscController { return _oscController; } @@ -1102,7 +1365,11 @@ static const double MOUSE_SPEED_DIVISOR = 1.25; // because the connection has not been established yet. } } - + + // Gamepads GameController doesn't expose. Devices already connected are + // matched while the run loop runs, after this initializer returns. + [self startHIDGamepads]; + if (@available(iOS 14.0, tvOS 14.0, *)) { for (GCMouse* mouse in [GCMouse mice]) { [self registerMouseCallbacks:mouse]; @@ -1145,35 +1412,13 @@ static const double MOUSE_SPEED_DIVISOR = 1.25; } [self unregisterControllerCallbacks:controller]; - self->_controllerNumbers &= ~(1 << controller.playerIndex); - Log(LOG_I, @"Unassigning controller index: %ld", (long)controller.playerIndex); - + Controller* limeController = [self->_controllers objectForKey:[NSNumber numberWithInteger:controller.playerIndex]]; if (limeController) { - // Stop haptics on this controller - [self cleanupControllerHaptics:limeController]; - - // Stop motion reports on this controller - [self cleanupControllerMotion:limeController]; - - // Stop battery reports on this controller - [self cleanupControllerBattery:limeController]; - - // Disassociate this controller from any controllers merged with it - if (limeController.mergedWithController) { - assert(limeController.mergedWithController.mergedWithController == limeController); - limeController.mergedWithController.mergedWithController = nil; - } - - // Inform the server of the updated active gamepads before removing this controller - [self updateFinished:limeController]; - [self->_controllers removeObjectForKey:[NSNumber numberWithInteger:controller.playerIndex]]; - - // Re-evaluate the on-screen control mode - [self updateAutoOnScreenControlMode]; - - // Notify the delegate - [self->_delegate gamepadPresenceChanged]; + [self removeController:limeController]; + } + else { + self->_controllerNumbers &= ~(1 << controller.playerIndex); } }]; @@ -1248,7 +1493,9 @@ static const double MOUSE_SPEED_DIVISOR = 1.25; _keyboardDisconnectObserver = nil; _controllerNumbers = 0; - + + [self stopHIDGamepads]; + for (Controller* controller in [_controllers allValues]) { [self cleanupControllerHaptics:controller]; [self cleanupControllerMotion:controller]; From cf9ad12ef6a1d6b9b518aec0de81381225758bd3 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 29 Oct 2026 20:47:32 +0100 Subject: [PATCH 09/13] Add the sandbox exception key for IOHIDLibUserClient The app container profile ignores com.apple.security.iokit-user-client-class and reads only com.apple.security.exception.iokit-user-client-class, so the first build's HID client was closed on open. With both keys the Xbox One S BLE controller works in a live stream on iPadOS 14.4. Document the IOHID fallback in the GameCult README section. Co-Authored-By: Claude Opus 5.5 --- GameCult/TrollStore.entitlements | 7 ++++++- README.md | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/GameCult/TrollStore.entitlements b/GameCult/TrollStore.entitlements index f174b9b..cd88ea6 100644 --- a/GameCult/TrollStore.entitlements +++ b/GameCult/TrollStore.entitlements @@ -4,10 +4,15 @@ + kIOReturnNotPermitted. The platform policy reads the first key; the app container + profile reads only the exception key. TrollStore keeps both in the binary. --> com.apple.security.iokit-user-client-class IOHIDLibUserClient + com.apple.security.exception.iokit-user-client-class + + IOHIDLibUserClient + diff --git a/README.md b/README.md index 2a4e138..a984753 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ This fork exists for one artifact: an **unsigned Moonlight IPA that installs wit - **Branch:** `gamecult/ios14-build`, based on upstream commit `159ffdb` (2026-09-26). That is the last commit before upstream raised the minimum to iOS 15 in `7c79fcc`, a change made for Xcode 27. - **Build:** `.github/workflows/gamecult-ios14-ipa.yml` runs on GitHub's macOS runner with Xcode 16 and uploads `Moonlight-ios14-unsigned.ipa` as a workflow artifact. - **Install:** with TrollStore, which signs the app itself. +- **Xbox One S on Bluetooth LE firmware:** iOS 14.4 connects this controller (VID `0x045E`, PID `0x0B20`, firmware 5.x) as a HID device, but GameController never exposes it, so stock Moonlight sees no controller. `Limelight/Input/ControllerSupport.m` adds an IOHIDManager source for it. It parses input report 1 (the layout SDL's HIDAPI Xbox One driver uses), feeds the same controller path as GameController pads, and drives rumble through output report 3. Controllers that GameController recognises are untouched. Opening the device needs the entitlement in `GameCult/TrollStore.entitlements` (`com.apple.security.iokit-user-client-class` naming `IOHIDLibUserClient`). The workflow embeds it with an ad-hoc signature, and TrollStore keeps it. - **Status:** a personal build, not a maintained distribution. For a supported Moonlight, use the [upstream app](https://github.com/moonlight-stream/moonlight-ios). Everything below is upstream's README. From 40ca1e2a81c352e15d8dc3f2fd3d18249d42d55c Mon Sep 17 00:00:00 2001 From: Kiko Date: Mon, 2 Nov 2026 16:55:52 +0000 Subject: [PATCH 10/13] README: publish health stays independent of every other feature Co-Authored-By: Claude Sonnet 5.5 --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 4b2cf99..e2323e8 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,6 @@ Check out [the Moonlight wiki](https://github.com/moonlight-stream/moonlight-doc * In the "Team" dropdown, select your name. If your name doesn't appear, you may need to sign into Xcode with your Apple account. * Change the "Bundle Identifier" to something different. You can add your name or some random letters to make it unique. * Now you can select your Apple device in the top bar as a target and click the Play button to run. + +## GameCult fork: publish health is independent +This fork carries the OBS bridge role that Ratatoskr used to hold. Whatever Moonlight publishes toward OBS, publish state is its own path, and no other feature may touch it. Sound, and any feature added after it, must never block, delay, or report on the publish path. A silent publish must never read as healthy: no output reads as unknown or failed, never as fine. From fa4cf9ea8d3e145d585f1c8ca5705f4615fb0769 Mon Sep 17 00:00:00 2001 From: Kiko Date: Thu, 5 Nov 2026 00:37:46 +0000 Subject: [PATCH 11/13] Add docs/remote-access.md: Tailscale and Headscale options (untested) Co-Authored-By: Claude Sonnet 5.5 --- docs/remote-access.md | 179 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 docs/remote-access.md diff --git a/docs/remote-access.md b/docs/remote-access.md new file mode 100644 index 0000000..5fe8f43 --- /dev/null +++ b/docs/remote-access.md @@ -0,0 +1,179 @@ +# Remote access: iPad to a Sunshine host on restrictive wifi + +**Status: planning document. Every step below is untested.** Nothing here has been run +against the real host, iPad, or network. Doc claims are cited; anything not found in the +cited docs is marked *unverified*. Nothing in this repo changes: this is a design note. + +## Problem + +Moonlight (this client) streams from a Sunshine host (called **Raven** here) over several +TCP and UDP ports. The target situation is an iPad on managed wifi that blocks those +ports, with no mobile data fallback. The usual escape is an overlay VPN whose traffic +looks like ordinary HTTPS. Only outbound TCP 443 is assumed to work. + +Sunshine's base port is 47989 and other ports are offset from it +([Sunshine configuration](https://docs.lizardbyte.dev/projects/sunshine/latest/md_docs_2configuration.html)). +That page does not list the offsets; read the exact list from Sunshine's config UI. Pairing +uses the Sunshine web UI (default `https://:47990`, PIN page) +([Sunshine getting started](https://docs.lizardbyte.dev/projects/sunshine/latest/md_docs_2getting__started.html)). +The web UI is LAN-only by default (`origin_web_ui_allowed: lan`); a tailnet address is not +the LAN, so pairing may need that setting changed or must be done on the LAN first. +*Unverified: whether tailnet source addresses count as `lan`.* + +## What the overlay must carry + +Moonlight video/audio/input are UDP. Tailscale and Headscale carry arbitrary IP inside +WireGuard, so UDP flows unchanged. When no direct path exists, the WireGuard packets +travel inside a TCP/HTTPS connection to a DERP relay. + +## Option A: Tailscale (hosted control plane, Tailscale DERP) + +### How it behaves on a 443-only network + +- Outbound requirements: TCP 443 for coordination and DERP; UDP 41641 and UDP 3478 only + improve direct connections. If only 443 is allowed, "connectivity still works" via + relays, but not as fast as peer-to-peer + ([Tailscale firewall ports](https://tailscale.com/kb/1082/firewall-ports)). +- Every connection starts relayed and then tries to upgrade to direct; DERP is the + fallback when direct fails. Relayed traffic is end-to-end WireGuard encrypted + ([connection types](https://tailscale.com/kb/1257/connection-types)). +- Tailscale warns DERP has limited quality of service and lower throughput. Its sample + `tailscale ping` output shows DERP at roughly 40 to 280 ms. Those are examples, not + predictions for this site. + +### Steps (all untested) + +1. Create or identify the tailnet and its owner (see Open Questions). +2. Install Tailscale on Raven; sign in to that tailnet. +3. Install the Tailscale iOS app on the iPad; sign in to the same tailnet. *Unverified: + whether the managed iPad's profile permits installing a VPN app or creating a VPN + configuration. This is the first likely blocker.* +4. In the Tailscale admin console or `tailscale status` on Raven, note Raven's tailnet IP + (`100.x.y.z`) or MagicDNS name. +5. On the LAN beforehand if possible, pair Moonlight with Raven normally. If pairing over + the tailnet: in Moonlight, add the host manually by tailnet IP; enter the PIN in + Sunshine's web UI. +6. Start a stream over the tailnet; check with `tailscale ping raven` (on any tailnet + device) whether the path is `direct` or `via DERP(...)`. +7. If relayed and unplayable, lower Moonlight resolution, FPS and bitrate first. + +### Latency limits when relayed + +Relayed means TCP-in-the-middle for UDP-like traffic: head-of-line blocking on loss, an +extra round trip through the nearest DERP region, and a throughput ceiling. Expect +playable desktop and slow-paced use, not guaranteed competitive-game latency. *No +measurement exists for this site; measure with `tailscale ping` and a trial stream.* + +## Option B: self-hosted Headscale on Yggdrasil behind 443 + +Headscale is an open-source reimplementation of the Tailscale control server; the +official Tailscale iOS app can point at it +([Apple clients](https://headscale.net/stable/usage/connect/apple/): account menu, +"Use custom coordination server", enter the `https://` URL). + +### Needs Eureka's approval first + +Nothing below is done until Eureka approves each item: + +- A new public hostname and DNS record for the control server. +- A public TLS certificate for it, and where TLS terminates. +- A new daemon (Headscale) on Yggdrasil and who deploys and survives it (Idunn + authority applies; this doc does not choose). +- Public exposure of 443 for this hostname, and possibly UDP 3478 (see below). +- A relay choice: reuse Tailscale's public DERP map (the iPad still reaches Tailscale's + relays on 443), or run Headscale's embedded DERP. +- Who may register nodes, and key expiry policy. + +### Minimal configuration shape (untested; read the example config for your release tag +before use, per [config docs](https://headscale.net/stable/ref/configuration/)) + +- `server_url`: the public `https://` hostname on 443. +- `listen_addr`: loopback or internal only; reverse proxy in front. +- TLS cert/key paths left empty when a proxy terminates TLS (Headscale then logs + `listening without TLS`, which is expected) + ([reverse proxy](https://headscale.net/stable/ref/integration/reverse-proxy/)). +- `trusted_proxies` set to the proxy address. +- A sqlite database path, a noise private key path, IP prefixes, and a DNS block as in the + shipped example config. +- `derp.urls`: keep Tailscale's default map for the first trial. Removing it makes the + embedded server a single point of failure + ([DERP](https://headscale.net/stable/ref/derp/)). +- Optional: `derp.server.enabled: true` plus `ipv4`/`ipv6`, `verify_clients: true`. + +### Proxy constraints from the docs + +- The proxy must support WebSockets: the control protocol upgrades via POST with + `Upgrade: tailscale-control-protocol`. Cloudflare proxies and Tunnels are unsupported. +- Port summary: tcp/443 public; tcp/80 only for built-in Let's Encrypt HTTP-01; udp/3478 + public if embedded DERP is enabled; tcp/50443 only for gRPC remote control; tcp/9090 + not public ([requirements](https://headscale.net/stable/setup/requirements/)). +- STUN for embedded DERP needs udp/3478, which an HTTPS reverse proxy cannot carry. On a + 443-only client network STUN is blocked anyway, so DERP-over-443 is the path that + matters. *Unverified: the docs do not say whether the embedded DERP's HTTPS endpoint + works behind a reverse proxy.* Simplest trial: leave Tailscale's DERP map in place. +- The reverse-proxy page is community maintained and may be outdated. + +### Exposure added + +One new public HTTPS hostname fronting a control plane; optionally a public relay and UDP +3478. Raven and the iPad gain no inbound ports; Sunshine ports stay on the tailnet. The +tailnet's ACLs should restrict the iPad to Raven's Moonlight ports only +(*unverified in this environment; ACL syntax differs between Headscale versions*). + +## Failure ladder + +Run top to bottom, stopping at the first success. Everything is untested. + +1. Home/LAN: confirm Moonlight streams to Raven with no overlay (baseline). +2. Tailscale on both devices at the LAN: confirm tailnet IP works, `tailscale ping` is + direct. +3. iPad on the restrictive wifi, Tailscale connected: does the app reach "Connected"? + If not: is VPN installation or profile blocked by management (stop; this is policy, + not network), or is 443 to the control plane filtered or TLS-intercepted? +4. `tailscale ping raven` from the iPad's side (use another tailnet device, or app + status): `direct` is best, `via DERP` means relay. +5. Relayed but stream stutters: lower bitrate, resolution, FPS; try H.264 over HEVC if the + iPad decoder is struggling (decode, not network, may be the limit). +6. Control plane reachable but no DERP: network may TLS-intercept or block Tailscale + domains. Option B with a hostname that is not a known Tailscale domain is the next step. +7. Wifi blocks everything but ordinary web browsing, and VPN apps are policy-blocked: + neither option works. Fall back to a browser-based path or a different network. + +## A or B + +| Question | A: Tailscale hosted | B: Headscale on Yggdrasil | +|---|---|---| +| Time to first test | Hours | Days (approvals, DNS, TLS, deploy) | +| New public exposure | None | Hostname, cert, daemon, maybe UDP 3478 | +| Works if network blocks Tailscale domains | No | Possibly (own hostname) | +| Works if network only does 443 | Yes, relayed | Yes, relayed, via Tailscale DERP or own | +| Control-plane ownership | Tailscale account holder | Eureka / GameCult | +| Latency when relayed | Tailscale DERP regions | Same, or own DERP region if run | +| Ongoing work | None | Upkeep, updates, backups | +| Recommended | Try first | Only if A fails at ladder step 3 or 6 | + +## Open Questions + +- Where does Raven sit on the network (same LAN as the iPad's home, behind carrier NAT, + behind a router that can forward ports, wired or wifi)? It decides whether direct + connections are plausible at all. +- Who holds the tailnet account for Option A (individual, org, shared), and who can add + devices and edit ACLs? +- Does the iPad's management policy allow a third-party VPN app? Nothing else matters if + not. +- Is any measurement available of latency from the restrictive network to the nearest + DERP region? +- If Option B is chosen: does Eureka want Headscale in the Yggdrasil deployment model, and + who owns it? + +## Sources + +- +- +- +- +- +- +- +- +- From 113e352789bd50c9f2664d2b20a9d51fe432517f Mon Sep 17 00:00:00 2001 From: Kiko Date: Thu, 5 Nov 2026 10:50:52 +0000 Subject: [PATCH 12/13] README: say the OBS bridge role is planned, not present Co-Authored-By: Claude Sonnet 5.5 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e2323e8..fa7ce53 100644 --- a/README.md +++ b/README.md @@ -24,4 +24,4 @@ Check out [the Moonlight wiki](https://github.com/moonlight-stream/moonlight-doc * Now you can select your Apple device in the top bar as a target and click the Play button to run. ## GameCult fork: publish health is independent -This fork carries the OBS bridge role that Ratatoskr used to hold. Whatever Moonlight publishes toward OBS, publish state is its own path, and no other feature may touch it. Sound, and any feature added after it, must never block, delay, or report on the publish path. A silent publish must never read as healthy: no output reads as unknown or failed, never as fine. +This fork is meant to take on the OBS bridge role that Ratatoskr used to hold; none of that code exists on this branch yet, so this is a rule for code to come. Whatever Moonlight publishes toward OBS, publish state is its own path, and no other feature may touch it. Sound, and any feature added after it, must never block, delay, or report on the publish path. A silent publish must never read as healthy: no output reads as unknown or failed, never as fine. From 634c72c5fbe139fc8d0aca2836c1da23d3e7c952 Mon Sep 17 00:00:00 2001 From: Kiko Date: Thu, 5 Nov 2026 10:51:02 +0000 Subject: [PATCH 13/13] docs: remote access conditions, Wireguard-only Sunshine, jailbroken iPad Co-Authored-By: Claude Sonnet 5.5 --- docs/remote-access.md | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/remote-access.md b/docs/remote-access.md index 5fe8f43..723654f 100644 --- a/docs/remote-access.md +++ b/docs/remote-access.md @@ -4,6 +4,36 @@ against the real host, iPad, or network. Doc claims are cited; anything not found in the cited docs is marked *unverified*. Nothing in this repo changes: this is a design note. +## Conditions in force + +Set by Eureka (who holds the Wireguard network and Yggdrasil). They bound every option +below; an option that cannot meet them is out. + +- **Raven's Sunshine host is a yes in principle only if** it is bound to the Wireguard + interface and paired by Meta, and never exposed (no public or LAN-wide listener, no + port forward). *Untested and unverified: how to bind Sunshine to one interface. This + doc has not found a Sunshine setting for it; check the Sunshine configuration page and + the host firewall before relying on either.* +- **Minds get no access to the iPad** until Meta gives its Yggdrasil address and says yes. + Nothing here is to be run from, or opened on, Yggdrasil before that. Option B below is + a design only. +- **Pairing is Meta's act**, not a Mind's. + +## Client device + +The iPad has no App Store and runs an old jailbroken iOS (Taurine). The fork reaches it +through TrollStore, not the App Store. Consequences, none of them tested: + +- Step 3 of Option A ("install the Tailscale iOS app") and the Headscale "custom + coordination server" path both assume the App Store app. *Unverified: whether any + Tailscale or Wireguard client can be installed on this iPad, and whether it runs on that + iOS version.* This is now the first likely blocker, ahead of any management profile. +- *Unverified: whether a VPN client installed via TrollStore can create a VPN + configuration on a Taurine device.* A Wireguard client on the iPad is the natural fit + for the conditions above, and it is equally untested. +- Whether this fork builds for that iOS version is a build question for the + `gamecult/ios14-build` branch, not settled here. + ## Problem Moonlight (this client) streams from a Sunshine host (called **Raven** here) over several @@ -45,7 +75,7 @@ travel inside a TCP/HTTPS connection to a DERP relay. 1. Create or identify the tailnet and its owner (see Open Questions). 2. Install Tailscale on Raven; sign in to that tailnet. -3. Install the Tailscale iOS app on the iPad; sign in to the same tailnet. *Unverified: +3. Install a Tailscale client on the iPad (no App Store there; see Client device); sign in to the same tailnet. *Unverified: whether the managed iPad's profile permits installing a VPN app or creating a VPN configuration. This is the first likely blocker.* 4. In the Tailscale admin console or `tailscale status` on Raven, note Raven's tailnet IP @@ -159,7 +189,7 @@ Run top to bottom, stopping at the first success. Everything is untested. connections are plausible at all. - Who holds the tailnet account for Option A (individual, org, shared), and who can add devices and edit ACLs? -- Does the iPad's management policy allow a third-party VPN app? Nothing else matters if +- Can any VPN client be installed on the Taurine iPad at all (App Store absent, TrollStore only)? Does its management policy allow a third-party VPN app? Nothing else matters if not. - Is any measurement available of latency from the restrictive network to the nearest DERP region?