Headless AirPlay receiver pipeline for WSL2 + NVIDIA. iPhone screen mirrors into OBS as a webcam-style source with no window on the Windows desktop, so it can't leak into a screen-share or alt-tab preview during a livestream or call.
Built on UxPlay (AirPlay 2 receiver), GStreamer (relay), and OBS Studio (consumer / virtual camera).
iPhone ─AirPlay─▶ UxPlay ─vdmp tap─▶ FIFO ─GStreamer─▶ TCP MPEG-TS ─▶ OBS Media Source ─▶ Virtual Camera ─▶ Zoom/Meet/Teams
(fakesink discards
decoded frames -
no window opens)
Hardware H264 decode in OBS via NVDEC. UxPlay still negotiates a decoder for AirPlay handshake reasons but its output goes nowhere. The raw H264 stream from the iPhone is intercepted before any decode and forwarded to OBS, which does the only meaningful decode in the chain.
A second branch of the GStreamer pipeline decodes the same stream
locally and pushes RGBA frames to a WSLg-projected ximagesink
window — useful for verifying frame flow without OBS in the loop.
A third branch produces a 10 fps JPEG ring at /tmp/phone-NNNNN.jpg
(max 8 files, rotated). Downstream consumers like the agent loop
(see agent/) and the tap-tester
(agent/tap-tester/) read from there.
-
Windows 11 with WSL2 (Ubuntu 22.04 or 24.04)
-
NVIDIA GPU + recent driver (RTX 30xx / 40xx / 50xx tested)
-
iPhone on the same Wi-Fi as the host
-
WSL2 mirrored networking. Add to
%USERPROFILE%\.wslconfig:[wsl2] networkingMode=mirrored [experimental] hostAddressLoopback=true
Then
wsl --shutdownfrom PowerShell.
git clone https://github.com/ellyseum/iphone_mirror_wsl ~/projects/iphone_mirror_wsl
cd ~/projects/iphone_mirror_wsl
./install.shinstall.sh installs apt deps (uxplay, avahi-daemon,
gstreamer1.0-{tools,plugins-base,plugins-good,plugins-bad,vaapi}),
checks for mirrored networking + an NVIDIA driver, and symlinks
./mirror into ~/.local/bin. Make sure ~/.local/bin is on
$PATH.
mirror start # bring up UxPlay + relay
mirror status # check pids and OBS connection info
mirror logs -f # follow logs (UxPlay's FPS data is here)
mirror test # open a tester window via WSLg to verify
mirror sdp # print the SDP path (UDP mode)
mirror stop # tear it all down
mirror restart # if iPhone reconnect gets stuckOn the iPhone: Control Center → Screen Mirroring → WSL-Mirror
(rename via RECEIVER_NAME in config.env).
- Sources → + → Media Source
- Uncheck "Local File"
- Input:
tcp://127.0.0.1:5004 - Input Format:
mpegts - Reconnect Delay: 1
- Tick "Restart playback when source becomes active"
- OK
Then Start Virtual Camera in OBS → Zoom/Teams/Meet pick "OBS Virtual Camera" in the camera dropdown.
If mirror status reports UDP mode instead, point the Media Source
at the SDP file printed by mirror sdp (Local File checkbox on, Input
Format mpegts).
Edit config.env, then mirror restart:
| Variable | Default | Notes |
|---|---|---|
RECEIVER_NAME |
WSL-Mirror |
Name iPhone shows in Screen Mirroring |
DISPLAY_RES |
3840x2160@60 |
Higher = iPhone uses higher bitrate |
FPS |
60 |
Cap |
DECODER |
nvh264dec |
avdec_h264 for software fallback |
TRANSPORT |
tcp |
tcp (reliable) or udp (low-latency, lossy) |
RELAY_PORT |
5004 |
|
RELAY_MCAST |
224.1.1.1 |
UDP mode only — multicast group, or set to a unicast consumer IP |
A user-scoped systemd unit ships at systemd/iphone-mirror.service.
Install it once:
mkdir -p ~/.config/systemd/user
cp systemd/iphone-mirror.service ~/.config/systemd/user/
systemctl --user enable --now iphone-mirror.serviceWSL2 needs systemd = true under [boot] in /etc/wsl.conf for
this to work.
agent/— closed-loop agent that reads the mirror's JPEG ring, asks a vision model for the next action, and types/taps back via a Bluetooth HID emulator. Independent of the relay path.agent/tap-tester/— browser-side tap calibration harness; the iPhone loads a page, the agent issues programmatic taps, the page reports back where the touch landed, and the server logs it.agent/training/— data-collection pipeline for training a cursor-finder CNN on synthetic samples. Active model training lives inios_pointer_finder.tester/— WSLg stream tester + a Python live-overlay that runs the cursor model on every JPEG-ring frame.extras/qvh/— vendored quicktime_video_hack for USB-side iPhone screen capture. Alternative to AirPlay; not the active path.
- See
CHANGELOG.mdfor shipped milestones. - A separate Electron app
(
cursor_finder_live) consumes this pipeline and runs the cursor-finder model live in the renderer with overlay UI. That replaces the olderiphone_mirror_vcamC# attempt.
- Stale pixels on static UI: H264 inter-frame compression means a bad reference frame can persist on unchanging screen regions until the next IDR keyframe (~2s on iPhone). Real-world content with motion repaints itself; UI screenshots don't. Use TCP transport (default) to avoid the source of corruption.
- iPhone disconnects after lock: AirPlay sessions die when the
phone screen locks.
mirror restartand reconnect. - First connect after
mirror startmay take 5–10s: GStreamer's TCP server has to receive enough buffer before OBS sees frames. - Mirrored networking is required: default WSL2 NAT mode hides WSL behind a private subnet that mDNS can't bridge cleanly.
GPL-3.0. Pulls in UxPlay (GPL-3) at runtime, so the same terms
apply downstream. Vendored upstreams keep their own licenses
(extras/qvh/LICENSE is MIT). See LICENSE for the full
text.