Skip to content

feat(mac): receiver mode — use this Mac as a display for another Mac - #145

Open
peetzweg wants to merge 4 commits into
mainfrom
feat-mac-mac
Open

feat(mac): receiver mode — use this Mac as a display for another Mac#145
peetzweg wants to merge 4 commits into
mainfrom
feat-mac-mac

Conversation

@peetzweg

Copy link
Copy Markdown
Owner

Closes #82 / #17. The Mac app gains a receiver role, switched with a tab bar at the top of the panel, so a spare Mac becomes a true extended display for another Mac.

Why: Mac→Mac is the cheapest platform expansion (both halves stay on Apple frameworks), and unlike AirPlay-to-Mac it's a real persistent extend, not mirroring.

How: The receiver core moves from iOS/PhoneReceiver.swift to Shared/StreamReceiver.swift (platform specifics injected: device kind, fallback name, cursor sprite as CGImage) and is reused unchanged by a new Mac/MacReceiver.swift — Bonjour listener on :9000, hello announcing the primary screen's framebuffer, video window with the cursor echo drawn AppKit-side, sleep suppression while receiving. The deliberate call: roles are exclusive (entering receiver mode stops outbound sessions, without flipping per-device auto-connect prefs), matching the tab-switch mental model from the issue. Sender side needed zero changes; no wire change (pv stays 2 — hello.device just says "Mac").

Verified end-to-end with two instances on one machine (-appRole receiver + -appRole sender -host 127.0.0.1): native 3024×1964 stream at ~14 ms p50, cursor echo positioned correctly, clean teardown after the disconnect grace. Display-only for now — KVM-style input forwarding from the receiving Mac is a follow-up. Note: conflicts with any sibling branch touching PhoneReceiver.swift (it moved).

peetzweg added 3 commits July 12, 2026 20:32
Move iOS/PhoneReceiver.swift to Shared/StreamReceiver.swift so the Mac
target can reuse the whole listen/deframe/decode/display pipeline. UIKit
specifics are injected instead of hardcoded: device kind and fallback
service name are init parameters, the cursor sprite crosses the callback
as a CGImage (ImageIO decode), and PeerUpdateSignal moves next to the
receiver that publishes it. New setPanel() generalizes the re-hello path
(iOS rotation funnels through it; macOS display-mode changes will use it
directly) and stop() supports leaving receiver mode. No wire change.
…82)

The Mac app gains a second role, switched with a tab bar at the top of
the panel: it can act as the receiving end exactly like the iPhone app —
Bonjour-advertised listener on :9000, hello announcing the primary
screen's framebuffer, shared StreamReceiver pipeline — so a spare Mac
shows up in the sending Mac's Devices list and works as a true extended
display at native (Retina) resolution.

ReceiverController owns the lifecycle: a fresh receiver per activation,
a resizable video window (native full screen via the green button) with
the local cursor echo drawn AppKit-side, sleep suppression while a
stream is live, and re-announcing on display-mode changes (the Mac
analogue of iPhone rotation). Roles are exclusive: entering receiver
mode stops outbound sessions without flipping per-device auto-connect
prefs, so switching back reconnects remembered devices. The sender side
needed zero changes.

Display-only for now — forwarding the receiving Mac's keyboard/trackpad
(KVM-style) is a follow-up (#17).
Real-hardware Mac→Mac testing surfaced two issues.

No cursor on the receiver: the sender sends the sprite once at connect
and then only when the cursor changes shape, but the video view (which
draws it) is only created once frames are on screen — after that first
sprite already passed, so a plain arrow stayed invisible forever. On
iOS the race is masked because hovering anything re-sends a changed
sprite. StreamReceiver now keeps the latest sprite and position and
both platform views replay them on attach (also heals the iOS
metal-toggle rebuild). The receiving Mac's own pointer is blanked while
over the video — the streamed cursor is the pointer that matters on
what acts as a monitor.

Letterboxed full screen: native full screen never covers the
menu-bar/notch strip on notched MacBooks, so the full-panel stream got
black bars on every side (and true 1:1 would put the remote menu bar
physically behind the notch). Announce the screen minus
safeAreaInsets.top instead — full screen is now exactly 1:1; notch-less
displays are unaffected (insets are zero, full panel as before).
@peetzweg

Copy link
Copy Markdown
Owner Author

Real-hardware feedback round (2027c63): the sender cursor was invisible on the receiving Mac (connect-time sprite raced the video view's creation — receiver now buffers and replays it on attach), the receiving Mac's own pointer is blanked over the video, and the announced panel drops the menu-bar/notch strip (safeAreaInsets.top) so native full screen is pixel-1:1 instead of letterboxed on every side. Notch-less displays announce the full panel as before.

Move PerfOverlay/FlowLayout/BarGraph (pure SwiftUI) from the iOS app
into Shared and pin the same HUD to the bottom of the Mac receiver's
video window, behind a "Performance overlay" toggle in the panel —
same showAnalytics default the iPhone uses. The overlay hosting view
carries the blank-cursor rect so the hidden local pointer stays hidden
over the HUD.
@peetzweg

Copy link
Copy Markdown
Owner Author

Added (1b4700d): the performance overlay is now available on the Mac receiver too — PerfOverlay moved to Shared/ and pinned to the bottom of the video window, behind a "Performance overlay" toggle in the panel (same showAnalytics default as iOS).

@clasalle-lila

Copy link
Copy Markdown

@peetzweg would love to help test. Is there a special way to compile the receiving end?

@peetzweg

Copy link
Copy Markdown
Owner Author

You just need to compile the desktop version. Both is in the same desktop app. You just need to switch one Desktop app to being the receiver.

Happy to get some user feedback on this as I don't have a "testflight" for the desktop app just yet.

But yeah this is a nice feture i can test well on my end on two macbooks. Worked pretty well. Did not wanted to yeet it in on that day. Will revisit this asap. :)

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.

Mac->Mac

2 participants