Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/windows.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Windows client
on:
push:
paths: ['Windows/**', '.github/workflows/windows.yml', 'Shared/Protocol.swift']
pull_request:
paths: ['Windows/**', '.github/workflows/windows.yml', 'Shared/Protocol.swift']
workflow_dispatch:
permissions:
contents: read
jobs:
build:
runs-on: windows-latest
defaults:
run:
working-directory: Windows
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
cache-dependency-path: Windows/package-lock.json
- run: npm ci
- run: npm test
- run: npm run test:smoke
- run: npm run dist
- name: Verify packaged application
run: |
$clientTest = Start-Process -FilePath .\dist\win-unpacked\iFrame.exe -ArgumentList '--smoke-test' -WorkingDirectory (Get-Location).Path -WindowStyle Hidden -Wait -PassThru
if ($clientTest.ExitCode -ne 0) { throw "Packaged app test failed: $($clientTest.ExitCode)" }
- uses: actions/upload-artifact@v4
with:
name: iFrame-Windows-x64
path: Windows/dist/iFrame-*-Windows-x64.exe
- uses: actions/upload-artifact@v4
if: always()
with:
name: Windows-test-screenshots
path: Windows/test-output/*.png
if-no-files-found: ignore
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,9 @@ iFrame.xcodeproj/
*.xcuserstate
xcuserdata/

# Windows client
Windows/node_modules/
Windows/dist/
Windows/test-output/
# Linux client
Linux/build/
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# iFrame

Low-latency Mac → iPad remote desktop for Apple silicon. It sends a video stream instead of pixel tiles.
Low-latency Mac → iPad or Windows remote desktop for Apple silicon. It sends a video stream instead of pixel tiles.

**Requirements:** a Mac with Apple silicon running macOS 14 or later (developed on an M4 Mac mini, macOS 15), an iPad on iPadOS 17 or later, Xcode, and [XcodeGen](https://github.com/yonaskolb/XcodeGen) plus `rsvg-convert` and ImageMagick if you regenerate icons (`brew install xcodegen librsvg imagemagick`).
**Host requirements:** a Mac with Apple silicon running macOS 14 or later (developed on an M4 Mac mini, macOS 15) and Xcode command-line tools.

**Clients:** an iPad on iPadOS 17 or later, or a Windows 10/11 x64 PC. Building the iPad app requires Xcode and [XcodeGen](https://github.com/yonaskolb/XcodeGen), plus `rsvg-convert` and ImageMagick if you regenerate icons (`brew install xcodegen librsvg imagemagick`). The [Windows client](Windows/README.md) uses Node.js to build and includes its runtime when packaged.

```
Mac (iframe-host) iPad (iFrame app)
Expand Down Expand Up @@ -78,6 +80,24 @@ See [Linux/README.md](Linux/README.md) for scaling, key mapping and hotkeys.
| Two-finger drag | Scroll |
| Three-finger tap | Toolbar (on-screen keyboard, stats, disconnect) |

## Client (Windows)

The [Windows client](Windows/README.md) connects to the same Mac host using H.264. It includes
Mac discovery, PIN login, virtual-display settings, keyboard/mouse input, fullscreen, and live stats.

On Windows 10/11 x64 with Node.js 22.12 or newer:

```powershell
cd Windows
npm ci
npm start
# Build a standalone portable Windows executable:
npm run dist
```

Open `Windows/dist/iFrame-0.1.0-Windows-x64.exe`, choose or enter your Mac's address, and enter
the host PIN. The packaged app includes its runtime. See [Windows controls, testing, and limitations](Windows/README.md).

## Notes
- The virtual display uses `CGVirtualDisplay`, a private CoreGraphics API (the same one DeskPad, BetterDisplay and Chromium's tests use). A future macOS could change it; run the host with `--no-virtual` to stream the Mac's existing display instead.
- `scripts/make-icon.py` generates the icon; `scripts/build-icons.sh` renders every icon size and the app's accent color from it.
Expand Down
68 changes: 68 additions & 0 deletions Windows/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# iFrame for Windows

A Windows 10/11 x64 client for the existing `iframe-host` on your Mac. The portable app includes its runtime; users do not need Node, Python, FFmpeg, or a separate codec installation.

## Connect

1. Start the Mac host using the instructions in the repository README. Grant its Screen Recording and Accessibility permissions.
2. Open `iFrame-0.1.0-Windows-x64.exe` from `Windows/dist`.
3. Choose a nearby Mac, or enter its IP address, Tailscale address, or hostname. The default TCP port is **7878**. Discovery uses mDNS on the local network; manual addresses also work when discovery is unavailable. Select **☆ Save** next to the port to keep an address under **Saved Macs**; select a saved Mac to fill in its address and port.
4. Enter the host PIN, choose the display size/density, and click **Connect to Mac**.

The app remembers saved Macs, the last address and display settings, but never saves the PIN. Preferences are stored in Electron's per-user app data directory. Windows may ask to allow local network access for discovery. The distributed executable is unsigned.

**Retina** requests a virtual Mac display at the selected pixel size with two pixels per Mac point. **Native** gives more desktop space. **Mirror** uses the Mac's existing display. Display size is chosen at connection time; resizing the app scales the picture without restarting the Mac display. Reconnect to change resolution or density.

## Controls

| Input | Action |
| --- | --- |
| Mouse / trackpad | Move, left/right/middle click, drag, vertical/horizontal scroll |
| Keyboard | Physical key passthrough using the Mac's keyboard layout |
| Ctrl (default) | Mac Command; Ctrl+C / Ctrl+V become Command+C / Command+V |
| Alt | Mac Option |
| Windows key, fullscreen | Mac Command; the Start menu and Windows+ shortcuts are captured while the stream is fullscreen and focused |
| Windows key, windowed (default mapping) | Mac Control, when Windows does not intercept it |
| Ctrl+Alt+F | Toggle fullscreen |
| Ctrl+Alt+R | Release keyboard focus to the toolbar |
| Ctrl+Alt+Q | Disconnect |
| Send text | Send pasted or typed Unicode text to the focused Mac field |
| Refresh | Request a new keyframe |

Disable **Use Ctrl for Mac Command shortcuts** for literal Ctrl → Control and Windows → Command mapping. Windows reserves shortcuts such as Alt+Tab, Windows+L, and Ctrl+Alt+Delete; these stay local. Held keys and mouse buttons are released when focus leaves the desktop. The host also releases input when the connection ends.

## Build and test

Requires Windows x64 and Node.js **22.12 or newer** with npm. Run from `Windows`:

```powershell
npm ci
npm test
npm run test:smoke
npm start
npm run dist
```

`dist/iFrame-0.1.0-Windows-x64.exe` is the portable executable. `dist/win-unpacked/iFrame.exe` can also be launched directly, provided its neighboring files remain in place. `npm run pack` builds only that unpacked directory. Packaging downloads Electron and the NSIS packaging tools on first use. The packaged app also accepts `--smoke-test` to run the integration test offscreen and write a result to `test-output` in the current directory.

The Node tests exercise Swift-compatible wire bytes, fragmented TCP reads, input mapping, malformed data, handshake, acknowledgements, and authentication failures. The Electron smoke test starts a loopback mock host, decodes real H.264 key and delta frames one at a time, checks pixels and acknowledgements, forwards input, and exercises disconnect/reconnect and PIN errors. It renders offscreen and writes screenshots to `test-output/`; it needs no Mac or network credentials.

`test/desktop.h264` is a synthetic four-frame test pattern, generated with:

```powershell
ffmpeg -f lavfi -i testsrc2=size=160x96:rate=60 -frames:v 4 -c:v libx264 -preset ultrafast -tune zerolatency -pix_fmt yuv420p -x264-params aud=1:keyint=60:bframes=0 -f h264 test/desktop.h264
```

FFmpeg is only needed to regenerate that fixture. Real Mac capture, permission behavior, mDNS across your network, and sustained high-resolution performance must be checked with a running Mac host.

## Implementation and limits

- Uses protocol version 1 without host changes: length-prefixed TCP, PIN hello, virtual-display request, video format/frame messages, decode ACKs, ping, input, and keyframe requests.
- Requests **H.264**. WebCodecs selects a compatible hardware or software decoder. A host forced to HEVC produces an actionable error; use automatic codec selection or `--codec h264` on the host.
- Converts four-byte AVCC NAL lengths to Annex B and injects SPS/PPS on keyframes. Frames are drawn directly to a canvas and acknowledged after decode and drawing, preserving the host's flow control. Malformed frames trigger throttled keyframe recovery; repeated decoder errors close the session.
- The renderer is sandboxed with context isolation, no Node integration, a restrictive content security policy, and a narrow validated IPC bridge. Socket framing is bounded at the host protocol's 32 MiB maximum.
- The host protocol is **not encrypted**. Use a trusted LAN or Tailscale; do not expose the host directly to the Internet.
- No audio, automatic clipboard synchronization, HEVC, or local remote-cursor overlay. The Mac cursor is part of the video. The Send text button is an explicit text transfer, not clipboard sync.
- This is an Electron client, not a native WinUI application. It includes Chromium and therefore has a larger distribution and memory footprint.

MIT, matching the repository license. Bundled Electron and dependency licenses are included in the distribution.
Binary file added Windows/assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading