feat(mac): receiver mode — use this Mac as a display for another Mac - #145
feat(mac): receiver mode — use this Mac as a display for another Mac#145peetzweg wants to merge 4 commits into
Conversation
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).
|
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 ( |
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.
|
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 |
|
@peetzweg would love to help test. Is there a special way to compile the receiving end? |
|
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. :) |
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.swifttoShared/StreamReceiver.swift(platform specifics injected: device kind, fallback name, cursor sprite as CGImage) and is reused unchanged by a newMac/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 (pvstays 2 —hello.devicejust 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 touchingPhoneReceiver.swift(it moved).