Skip to content

Support iPhone Duo and adopt the UIScene life cycle - #730

Open
JR-Yu6197 wants to merge 7 commits into
moonlight-stream:masterfrom
JR-Yu6197:iphone-duo
Open

JR-Yu6197 wants to merge 7 commits into
moonlight-stream:masterfrom
JR-Yu6197:iphone-duo

Conversation

@JR-Yu6197

@JR-Yu6197 JR-Yu6197 commented Oct 29, 2026 •

Copy link
Copy Markdown

iPhone Duo runs the current App Store build letterboxed on its inner display (#728), and master built with the iOS 27 SDK is terminated at launch because it doesn't adopt the scene life cycle. This PR fixes both and makes streaming fit iPhone Duo's displays. The commits are separate steps that each build on their own, so I'm happy to split them into separate PRs or drop any of them.

  1. Adopt the UIScene life cycle. Adds a scene manifest (with an iPad variant for the iPad storyboard) and a scene delegate that handles home screen quick actions. This is iOS only; tvOS is covered by Adopt the UIScene lifecycle on tvOS #719.
  2. Lay out the stream and menus again when the display changes. Folding or unfolding iPhone Duo moves the scene to a display of a different size. The stream view, video layer, labels, stats overlay and on-screen controls now follow the view size, which covers what you asked for in Enable external display support on iPad by allowing the OS to control the window shape and size. #608 (comment). The on-screen controls keep held buttons and sticks across relayouts, use the large layout on regular-size displays, and stay off the fold while the device is partially folded.
  3. Create the stream display link from the window scene, so frame pacing follows the display the stream is on. It's created and invalidated on the main thread, which can't deadlock because LiStopConnection() is always called on a background queue (Connection.m).
  4. Match the Full and Safe Area resolutions to the current display. They're resolved when a stream starts instead of when settings were opened, use the native scale, and are rounded to even sizes (the inner display used to request 2853x2007). Sizes saved by older versions are still recognized.
  5. Fix mouse input on iPhone with AssistiveTouch. Clicks come from UIKit until GCMouse delivers button events, and the system pointer stays hidden over the stream on iPhone. This addresses the iPhone case of Double cursor when using Bluetooth mouse on iPhone 16 Pro Max (iOS 26.5) #711; iPad behavior is unchanged.
  6. Require a deliberate swipe to end the stream. A finger swipe from the left edge now has to be completed and cover at least 100 pt, so taps near the edge (like on a dock on the host's desktop) no longer disconnect.
  7. Add a video scaling option (Fit, Fill, Stretch) for displays whose aspect ratio differs from the host's, like the 1.42:1 inner display. These are the letterbox, crop and stretch modes from Pinch to zoom and full screen on iPhone X/xr #374. The setting is stored in a new Core Data model version (v1.11) that migrates automatically.

Notes:

Testing with Xcode 27.1:

  • iPhone Duo (iOS 27.2) on the inner display, streaming from Apollo, including a Bluetooth mouse through AssistiveTouch
  • iPhone 15 Pro Max (iOS 27.2)
  • iPad Pro 11-inch simulator (iPadOS 27.0): launch and main screen
  • The CI workflow passes on my fork for iOS and tvOS, Debug and Release (run), and every commit builds for iOS

iPhone Duo owners only have the letterboxed build for now, so I'd be very grateful if this could make it into the next App Store release. I'll follow up quickly on any review feedback.

Apps built with the iOS 27 SDK are terminated at launch unless they adopt
the scene-based life cycle. Add a scene manifest (with an iPad variant for
the iPad storyboard) and a scene delegate that handles home screen quick
actions, and let the alert and loading frame presenters find a window
before one has become key.
iPhone Duo moves the scene between displays of different sizes when it is
folded or unfolded mid-stream, and iPad windows can be resized. Keep the
stream view, video layer, labels and on-screen controls sized to the view,
use the large screen control layout on displays that are regular-sized in
both dimensions, and keep controls off active fold (division) regions.

Also opt the stream out of the vertical bar, release on-screen controls
when touches are cancelled, clamp the frame rate to the display the stream
starts on, and keep the host list, app tiles, loading spinner and settings
padding right across display changes. Fill the area above the navigation
bar with the bar color instead of leaving a black band at the top of the
screen.
A display link from the window scene is retargeted when the scene moves to
another display, like when iPhone Duo is folded or unfolded mid-stream, so
frame pacing follows the display the stream is on.

The window scene can only be read on the main thread, so create the display
link there, and invalidate it there too so no callback can run after stop
returns. This can't deadlock because LiStopConnection() is always called on
a background queue (see Connection.m), so the main thread never waits for
the renderer to stop.
Save the full screen and safe area options as special values that are
resolved when a stream starts, so they match whichever display the stream
is on (like the inner and outer displays of iPhone Duo) instead of the one
the settings were opened on. Compute them from the native scale, round
them to the even dimensions video encoders require (the inner display of
iPhone Duo used to request 2853x2007), and leave room for the larger safe
area inset on both sides since the video is centered.

Also recognize sizes saved by older versions as these options, keep custom
resolutions custom even when they match the current display, and round
custom resolutions to even dimensions.
iPhone only supports mice through AssistiveTouch. It reports the mouse
through GCMouse but doesn't always deliver its buttons there, and the
system pointer was drawn on top of the host's cursor. On iPhone, fall back
to the UIKit pointer button events until GCMouse delivers a button event
and then use only GCMouse's, and keep the system pointer hidden over the
stream while GCMouse moves the cursor.
Swiping from the left edge ended the stream as soon as the swipe was
recognized, so tapping or dragging near that edge, like on a dock at the
side of the host's desktop, could disconnect by accident. Only end the
stream when a swipe from the edge is completed and covers at least 100
points, and only accept finger touches so dragging a mouse along the edge
doesn't count.
Let users fill displays whose aspect ratio differs from the host's, like
the 1.42:1 inner display of iPhone Duo streaming a 16:9 desktop. Fit keeps
the current behavior, Fill crops the edges of the video that don't fit and
Stretch distorts the video to fill the screen.

StreamView now computes the video region for every mode, and the video
layer, touch and mouse mapping and pointer region all use it. The setting
is stored in a new Core Data model version (Moonlight v1.11), which is
migrated automatically.
@Grippy98

Grippy98 commented Nov 12, 2026 •

Copy link
Copy Markdown

Tested on my Duo and wanted to confirm it works and looks very well. Nice job! Even closing and opening the hinge works seamlessly which is quite impressive.

I'm not sure if this was a thing previously on IOS at all but is menu navigation supposed to work with a controller? Not a huge deal either way obviously the screen is there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants