Mojo documentation
Mojo streams a computer's monitors to another computer with hardware video encoding and sends your mouse, keyboard, clipboard and files back. Unlike most remote desktops, the viewer can ask the remote computer to create extra monitors that don't physically exist.
Mojo has three programs and one app:
- mojo-host runs on the computer you want to reach. It captures monitors, creates virtual ones, encodes video and injects input.
- mojo-client is the viewer. It opens one window per remote monitor.
- mojo-relay is optional. It introduces hosts and viewers by device ID across the internet and relays traffic when a direct path is impossible.
- Mojo (the desktop app) wraps the host and viewer with a device list, a monitor planner and one-click sharing.
Install
Download the mojo-windows-x64 artifact from the latest CI run and unzip it anywhere. It contains:
Mojo.exe— the desktop app (needs the WebView2 runtime, which Windows 10 and 11 already have)mojo-client.exe,mojo-host.exeand the FFmpeg DLLs they useconnect.batandshare-this-pc.bat— edit and double-click shortcuts
Virtual monitors on a Windows host need the SudoVDA virtual display driver installed once. Without it, the host still shares its physical monitors.
Mojo uses the system FFmpeg libraries on macOS. Install them with Homebrew, then build:
brew install ffmpeg pkg-config
git clone https://github.com/moderniselife/MojoRDP && cd MojoRDP
cargo build --release -p mojo-host -p mojo-client
Apple silicon builds are also attached to every CI run as mojo-macos-arm64. The host needs macOS 13 or later (system audio capture needs ScreenCaptureKit audio; virtual monitors need macOS 11 or later).
Build from source. On Ubuntu 24.04:
sudo apt install clang libclang-dev pkg-config \
libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libavfilter-dev libavdevice-dev \
libx11-dev libxcb1-dev libxcb-shm0-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xfixes0-dev \
libxkbcommon-dev libwayland-dev libvulkan-dev libpulse-dev libasound2-dev \
libpipewire-0.3-dev libspa-0.2-dev
git clone https://github.com/moderniselife/MojoRDP && cd MojoRDP
cargo build --release
Or build inside docker/dev.Dockerfile, which has every dependency including an NVENC-enabled FFmpeg.
Connect to a computer
Find hosts on your network, then connect by name or address:
mojo-client discover
mojo-client 192.168.1.20 --password 'pick-a-long-passphrase'
The first connection remembers the host's certificate fingerprint (trust on first use). If the fingerprint ever changes, the viewer refuses to connect. After a legitimate reinstall of the host, run once with --forget-host. To require a specific fingerprint from the start, pass --pin AA:BB:… (print it on the host with mojo-host --print-id).
If the connection drops, the windows stay open and the viewer reconnects with the same layout.
Monitors
By default the viewer mirrors the host's primary monitor. Every --virtual asks the host to create another monitor for this session; they disappear when you disconnect.
# Primary monitor plus two new ones
mojo-client studio-pc --password '…' --mirror primary \
--virtual 2560x1440@144 --virtual 1920x1080@60
# Every existing monitor, at 120 fps
mojo-client studio-pc --password '…' --mirror all --fps 120
# Only virtual monitors (the physical screens stay private)
mojo-client studio-pc --password '…' --virtual 3840x2160@60
Each monitor gets its own window and its own encoder, so a 144 fps monitor and a 60 fps monitor don't slow each other down. In the desktop app, the monitor planner shows the remote arrangement: click physical monitors to include them and add virtual ones with resolution and refresh chips.
No monitor attached? A host with no screen plugged in (a server, a GPU box in a cupboard, a cloud VM) creates a 1920×1080 virtual display by itself, so there is always a desktop to connect to. Choose its size with --headless-display 2560x1440@120.
Clipboard and files
Clipboard
Copy on either computer, paste on the other. Text syncs instantly. Copied files and folders are sent in the background and placed on the other computer's clipboard, so a normal paste in Explorer, Finder or a Linux file manager drops them there. Copies over 2 GB are skipped; drag those instead. Turn sync off with --no-clipboard.
Drag and drop
Drag files or folders from your computer onto a remote monitor's window. Mojo sends them and puts them where you let go:
| Dropped on | Lands in |
|---|---|
| A File Explorer window (Windows) | That window's folder |
| A Finder window (macOS) | That window's folder |
| A file manager window on X11 (Nautilus, Nemo, Thunar, Caja, PCManFM, Dolphin) | That window's folder |
| The desktop | The Desktop folder |
| Anything else, or GNOME on Wayland with several folders open | Downloads |
Existing files are never overwritten; a second copy becomes report (2).pdf. Paths that try to escape the destination are rejected. The viewer's title bar confirms where the files were saved. For scripts, --send PATH uploads to the host's Downloads.
Audio and cursor
The host streams its desktop audio as 48 kHz stereo Opus in 10 ms frames; the viewer keeps a 30 ms jitter buffer. Use --no-audio on the viewer or --audio none on the host to turn it off.
The remote pointer is not drawn into the video. Instead the host sends each new pointer shape, and the viewer makes it your own OS cursor, which moves with zero network latency. --video-cursor switches back to drawing it into the video, which some games prefer.
Viewer hotkeys
| Keys | Action |
|---|---|
| Ctrl Alt Shift F | Toggle fullscreen for this monitor's window |
| Ctrl Alt Shift G | Lock the mouse to the window and send raw movement (games); press again to release |
| Ctrl Alt Shift S | Show live stats (fps, bitrate, latency) in the title bar |
| Ctrl Alt Shift R | Start or stop recording (saved to Videos/Mojo) |
| Ctrl Alt Shift P | Privacy mode: darken the host's screens and lock its keyboard and mouse |
| Ctrl Alt Shift V | Send your microphone to the host |
| Ctrl Alt Shift Del (or End) | Ctrl+Alt+Del on the remote computer |
| Ctrl Alt Shift Q | Disconnect |
Move the pointer to the top edge of a window to reveal the session bar with the same actions.
Phones, tablets and TVs
The Android app runs on phones, tablets, Chromebooks and Android TV; the Apple app on iPhone, iPad and Apple TV. Computers sharing with Mojo on your network show up automatically; you can also type an address or device ID.
- Trackpad mode (default): drag to move the pointer, tap to click, tap then drag (Android) or long-press then drag (iPhone, iPad) to drag, two-finger tap to right-click, two-finger drag to scroll, pinch to zoom, three-finger tap for the keyboard.
- Direct mode: tap where you want to click, long-press to right-click. Switch in Settings or the in-session menu.
- Keyboard: the on-screen keyboard types any language (it's sent as text), and the key bar above it adds Esc, Tab, Ctrl, Alt, Win, Shift, arrows, Home, End, Del and F1–F12. Ctrl, Alt, Win and Shift stay held for the next key, so Ctrl then C copies. Hardware keyboards and mice work too.
- Displays: show the main display, all displays (side by side on tablets and TVs, one at a time on phones), or New display, which adds a virtual monitor to the computer at your device's resolution and refresh rate.
- The menu: Back on Android, Menu on the Siri Remote, the handle at the top of the screen on iPhone and iPad, or Start + Back on a controller. It has the keyboard, display switcher, touch mode, Ctrl+Alt+Del, "type clipboard", stats and Disconnect.
- Clipboard: text you copy on the computer lands on the device's clipboard; text you copy on the device goes to the computer when you return to Mojo.
Controllers
Up to four controllers per viewer. On Windows (with ViGEmBus installed) and Linux (with access to /dev/uinput) each one appears to games as an Xbox 360 controller. Everywhere else, or when you set Controllers to Mouse & keyboard, a controller drives the desktop:
| Control | Does |
|---|---|
| Left stick | Move the pointer (hold LT for fine control) |
| Right stick | Scroll |
| A / B | Left click / right click |
| D-pad | Arrow keys |
| X | Backspace |
| Y | On-screen keyboard |
| Start / Back (Menu / View) | Enter / Escape |
| LB / RB | Browser back / forward |
| Start + Back together | Mojo's menu |
TV remotes work without a controller: on Android TV the D-pad moves the pointer and OK clicks (switch the remote to arrow keys in the menu); on Apple TV the Siri Remote's touch surface moves the pointer, a click clicks and Play/Pause right-clicks.
Games
- Frame rate: up to 240 fps per monitor (
--fps 240, or a 240 Hz virtual monitor). At 1080p240 a frame reaches the viewer about 5 ms after capture starts. - Relative mouse: shooters and 3D games need raw mouse movement, not a pointer position. Turn on Lock the mouse for games in Settings, pass
--relative-mouse, or press Ctrl Alt Shift G during a session. The pointer is locked to the window and every movement is sent as-is. - Rumble: when a game vibrates its controller, the viewer's controller vibrates too (desktop viewers, Android, iPhone, iPad and Apple TV with a controller that has haptics).
- Sharp text for strategy games and work: Pixel-sharp text (4:4:4) in Settings or
--444sends full-resolution colour. It needs about half again as much bandwidth; hosts without a 4:4:4 hardware encoder fall back to software up to 1080p, or to normal colour.
Chat, unattended access and pairing
Chat
In the desktop app, press Chat next to a live session (or next to a viewer in Share) to message the person at the other end. Their messages pop the chat open and bring the app forward. On phones, tablets and TVs, Chat is in the in-session menu, and new messages show briefly over the picture.
Wake-on-LAN
Hosts advertise their network card's address. Once a viewer has seen a computer, it can wake it later: Wake it up in the desktop app, Wake on Android and iOS, or mojo-client HOST --wake. The computer needs Wake-on-LAN enabled in its firmware and network settings.
Unattended access
In the desktop app's Share tab, turn on Unattended access. Mojo then runs as a background service with your sharing settings, whether or not the app is open:
- Windows: a system service (Windows asks for administrator permission once). From another device you can sign in, unlock the lock screen, answer UAC prompts and press Ctrl+Alt+Del (Ctrl Alt Shift Del on desktop viewers, or the in-session menu on phones and TVs). It starts with Windows and moves with you when users switch.
- macOS and Linux: Mojo keeps running whenever you're signed in (a LaunchAgent, or a systemd user service).
Turn on Same password every time first if you want to connect with a password; otherwise only paired devices can connect. On the command line: mojo-host --service install --password …, --service status, --service uninstall.
Pair a device
Press Pair a device in Share (or run mojo-host --pair, or mojo-host --service pair for the background service). Enter the 6-digit code on the other device: Pair with a code on the connect screen of the desktop, Android and Apple apps, or mojo-client HOST --pair 123456. It works for 5 minutes, once. After that, that device connects without the password. Remove it from the paired list any time.
Privacy mode, recording and microphone
- Privacy mode darkens the host's screens and locks its keyboard and mouse while you work; you still see everything. Windows shows a "being used remotely" curtain. Ctrl+Alt+Del on the host always works as a way out.
- Record saves the session exactly as it streamed (video and sound, no re-encode) as a
.tsfile that VLC, mpv and editors open. Desktop: Videos/Mojo. Android: Movies/Mojo. iPhone and iPad: Files › On My iPhone › Mojo. - Microphone sends your voice to a virtual microphone on the host, so calls and voice chat there hear you. Linux hosts create "Mojo Microphone" automatically; Windows needs the free VB-CABLE and macOS the free BlackHole.
Over the internet
On a LAN, Mojo connects directly. Between networks, hosts register with a rendezvous server and get a 9-digit device ID:
mojo-host --password '…' --rendezvous relay.example.com
# logs: device_id=530 437 196
mojo-client 530437196 --password '…' --rendezvous relay.example.com
Both sides then try to reach each other directly through their NATs from the same UDP socket they will stream on. Most home routers allow this. When they don't (carrier-grade or symmetric NAT), traffic flows through the relay. Set MOJO_RENDEZVOUS instead of passing the flag every time.
Host a relay
Any small VPS works; introductions need almost no CPU. Relayed sessions use bandwidth equal to the stream bitrate, around 10–20 Mbps for 1080p60.
git clone https://github.com/moderniselife/MojoRDP && cd MojoRDP
docker build -f docker/relay.Dockerfile -t mojo-relay .
docker run -d --name mojo-relay --restart unless-stopped --network host \
-v mojo-relay:/data mojo-relay --public-ip <VPS_PUBLIC_IP> --relay-ports 40000-40999
Open UDP 9460 for rendezvous and UDP 40000–40999 for relaying (two ports per relayed session). Without Docker, cargo build --release -p mojo-relay; the relay needs no FFmpeg or GPU.
Security
- Every session is QUIC with TLS 1.3 between viewer and host. Relays forward packets they cannot read.
- Each host has a persistent certificate. Viewers remember it on first connect and refuse a different one;
--pinenforces a fingerprint up front. - Hosts require a password (a random one unless you set your own), check it in constant time, and lock out an address after repeated failures.
--approveadds a per-viewer prompt. - Pairing uses SPAKE2 with a one-time 6-digit code, bound to the host's certificate so an impostor in the middle can't complete it. Each paired device gets its own token; the host keeps only a hash and can revoke it.
- Device IDs are tied to the host's certificate: the relay refuses mismatched registrations and viewers verify the pairing.
- Every network decoder is size-bounded and fuzz-tested.
- Pass secrets through
MOJO_PASSWORDinstead of the command line so they don't show up in process lists. - Files received from a viewer can never overwrite existing files or leave the destination folder.
--insecure-no-password flag, meant for isolated test rigs. The full model and known gaps are in SECURITY.md.Performance
The viewer measures every frame from the moment the host started capturing it to the moment a decoded picture is ready. Medians on an RTX 3070 with host and viewer on the same machine:
| Codec | Resolution | fps | Mbps | Encode | Decode | Capture → decoded | p99 |
|---|---|---|---|---|---|---|---|
| H.264 | 1280×720 | 144 | 9.4 | 1.5 ms | 1.0 ms | 2.6 ms | 3.1 ms |
| H.264 | 1920×1080 | 144 | 21.2 | 3.1 ms | 1.8 ms | 5.0 ms | 5.4 ms |
| H.264 | 2560×1440 | 144 | 37.7 | 5.7 ms | 3.2 ms | 9.3 ms | 12.3 ms |
| H.264 | 3840×2160 | 60 | 50.0 | 13.5 ms | 6.8 ms | 20.8 ms | 24.7 ms |
| HEVC | 1920×1080 | 144 | 14.8 | 3.0 ms | 1.2 ms | 4.3 ms | 4.8 ms |
| HEVC | 2560×1440 | 144 | 26.3 | 5.3 ms | 1.9 ms | 7.3 ms | 8.2 ms |
| HEVC | 1920×1080 | 240 | 29.1 | – | – | 5.0 ms | 8.9 ms |
| HEVC | 2560×1440 | 240 | 48.2 | – | – | 9.1 ms | 16.0 ms |
Tuning
--codec h265cuts bitrate by about a third at the same quality when both ends have HEVC hardware.--bitratecaps each stream in kbps. By default Mojo picks a bitrate from resolution and frame rate, then adapts to loss and queueing.- If the viewer can't decode fast enough, the host lowers that stream's frame rate instead of letting frames queue up.
--vsyncremoves tearing at the cost of up to one frame of latency.--no-hwdecforces software decoding (useful to rule out driver problems).mojo-host --list-encodersshows which encoders this build can use;--encoderforces one.
Benchmark your own setup headlessly: mojo-client HOST --bench 10 prints a JSON report.
Bad networks
Mojo is tested against simulated loss, jitter, distance and bandwidth caps (scripts/netem-matrix.py). 1080p60 HEVC, 15 seconds per condition; latency includes the simulated network delay:
| Network | fps | p50 | p95 | Frames lost | Keyframes |
|---|---|---|---|---|---|
| Clean LAN | 60/60 | 4.7 ms | 5.6 ms | 0 | 0 |
| 1% packet loss | 60/60 | 4.7 ms | 5.2 ms | 2 | 0 |
| 5% packet loss | 59/60 | 5.7 ms | 6.2 ms | 4 | 0 |
| 10% bursty loss | 60/60 | 4.7 ms | 5.2 ms | 3 | 0 |
| Wi-Fi jitter (2 ms ± 4 ms) | 60/60 | 12.0 ms | 14.8 ms | 0 | 0 |
| Same city (10 ms each way) | 60/60 | 15.7 ms | 16.3 ms | 0 | 0 |
| Cross-country (35 ms each way, 1% loss) | 59/60 | 41.2 ms | 42.9 ms | 2 | 0 |
| Bad hotel Wi-Fi (20 ms, 8 ms jitter, 3% loss) | 59/60 | 29.5 ms | 31.7 ms | 2 | 0 |
| Capped at 8 Mbit/s | 60/60 | 4.7 ms | 5.5 ms | 0 | 0 |
Forward error correction rebuilds most damaged frames. When one can't be rebuilt, the viewer tells the host the last frame it has; the encoder drops the lost references and the very next frame repairs the picture, so no keyframe burst is needed. Rate control watches both queueing delay and delivered throughput and starts gently, so on a capped link it settles just under the cap instead of filling the router's queue. Details in BENCHMARKS.md.
Command line
mojo-host
| Flag | Default | What it does |
|---|---|---|
--password | random | Password viewers must give; a random one is printed if you don't set one. Env: MOJO_PASSWORD. |
--approve | off | Ask on the terminal (or in the app) before each viewer gets in. |
--insecure-no-password | off | Accept anyone who can reach the host. Test rigs only. |
--bind | 0.0.0.0:9450 | Address and UDP port to listen on. |
--source | auto | auto, x11, gnome, windows, macos or synthetic (test pattern). |
--display | $DISPLAY | X display to capture (X11 only). |
--synthetic-size | 1920x1080 | Resolution of the synthetic source. |
--encoder | auto | Force an encoder, such as h264_nvenc or libx264. |
--max-fps | 240 | Upper bound for any stream's frame rate. |
--audio | auto | auto (system output), tone (440 Hz test) or none. |
--rendezvous | none | Rendezvous server for device-ID connections. Env: MOJO_RENDEZVOUS. |
--no-advertise | off | Don't announce on the local network. |
--config-dir | OS config dir | Where the host identity lives. Env: MOJO_CONFIG_DIR. |
--print-id | Print device ID and certificate fingerprint as JSON, then exit. | |
--list-encoders | Print encoders in this build, then exit. | |
--headless-display | 1920x1080@60 | With no monitor attached, create a virtual display of this size (and again if every display goes away). off disables it. |
--pair | Show a 6-digit pairing code for 5 minutes. --list-paired and --unpair ID manage paired devices. | |
--service ACTION | install [flags], uninstall, status or pair: run the host as a background service (Windows: as administrator). | |
--view-only, --no-clipboard | off | Viewers watch without control; don't sync the clipboard. |
mojo-client
mojo-client HOST [flags], where HOST is an address, a host name, or a 9-digit device ID with --rendezvous. mojo-client discover lists hosts on the LAN.
| Flag | Default | What it does |
|---|---|---|
--password | none | Host password. Env: MOJO_PASSWORD. |
--mirror | primary | primary, all or a display ID. Default only when no --virtual is given. |
--virtual WxH@FPS | Create a virtual monitor on the host. Repeat for more. | |
--fps | 60 | Frame rate for mirrored monitors. |
--codec | h264 | h264, h265 or av1. |
--bitrate | 0 (auto) | Per-stream bitrate cap in kbps. |
--fullscreen | off | Open windows fullscreen. |
--vsync | off | Present on vsync (no tearing, up to a frame more latency). |
--no-hwdec | off | Decode in software. |
--no-audio | off | Don't play desktop audio. |
--no-clipboard | off | Don't sync the clipboard. |
--video-cursor | off | Draw the remote pointer into the video. |
--relative-mouse | off | Start with the mouse locked and raw movement (games). |
--444 | off | Full-resolution colour (4:4:4) for sharp text. |
--wake | off | Send Wake-on-LAN to a remembered host before connecting. |
--rendezvous | none | Rendezvous server for device IDs. Env: MOJO_RENDEZVOUS. |
--force-relay | off | Skip the direct attempt and use the relay (testing). |
--pin | Require this SHA-256 certificate fingerprint. | |
--forget-host | off | Forget the remembered certificate for this host. |
--send PATH | Upload files or folders to the host's Downloads. Repeatable. | |
--bench SECONDS | Headless benchmark with a JSON report. | |
--dump PREFIX | With --bench: save one decoded frame per stream as PPM. |
mojo-relay
| Flag | Default | What it does |
|---|---|---|
--bind | 0.0.0.0:9460 | Rendezvous address and UDP port. |
--public-ip | auto | Public IP to hand out for relay ports. |
--relay-ports | any | UDP port range for relay sockets, such as 40000-40999. |
--relay-idle-secs | 60 | Close a relay after this long without traffic. |
--no-relay | off | Introductions and hole punching only. |
Platform support
The goal is identical features on Linux, Windows and macOS, on both ends. The matrix on the home page shows each feature's status per OS. In short:
- Linux host: X11 and GNOME (Wayland or X11) tested end to end, including virtual monitors, input, audio, cursor, clipboard and files. KDE Wayland is planned.
- Windows host and viewer: tested in a Windows 11 VM: DXGI capture, SudoVDA virtual monitors, SendInput including Unicode text, folder drops, the desktop app and the installer. Software encoding is used when there's no GPU. The D3D11 zero-copy path is behind
MOJO_WIN_GPU=1. - macOS host and viewer: tested in a macOS 14 VM: ScreenCaptureKit capture, VideoToolbox encoding, CGEvent input and Unicode text, the permissions flow. Virtual monitors (CGVirtualDisplay) still need testing on a real Mac.
- Android and Android TV: tested on emulators against a Linux host: streaming, touch, keyboard, controllers, reconnects.
- iPhone, iPad and Apple TV: the app typechecks against the iOS SDK and runs (as a Mac Catalyst build) streaming from a macOS host. Device builds need Xcode.
Troubleshooting
The viewer says the host certificate changed
Either the host was reinstalled or its config folder was deleted, or someone is impersonating it. If you know it was reinstalled, connect once with --forget-host.
Virtual monitors look sheared or stretched on X11
Some compositors (xfwm4's) don't handle outputless RandR monitors. Disable compositing, or use a window manager such as Openbox. GTK desktops may ignore RandR monitors that have no output; the GNOME backend avoids this entirely.
Black screen or no input on macOS
Check Screen Recording and Accessibility for the host in System Settings → Privacy & Security, then restart the host. macOS only applies those permissions to new processes.
Hardware encoding isn't used
Run mojo-host --list-encoders. Mojo tries hardware encoders first and checks each with a test frame before using it; anything that fails falls back to x264. The host log names the encoder each stream opened.
Can't connect across the internet
Make sure both sides use the same --rendezvous server and that UDP 9460 (and the relay port range) are open on it. --force-relay tests the relay path on its own.
Architecture
Mojo is a Rust workspace: mojo-proto (wire protocol, forward error correction, clock sync), mojo-net (QUIC, certificates, congestion control, discovery, clipboard and file transfer), mojo-codec (FFmpeg hardware encode and decode, colour conversion, Opus), mojo-host, mojo-client and mojo-relay, plus the Tauri desktop app.
Each monitor runs a two-stage pipeline on the host: capture and colour conversion on one thread, upload, encode and send on another, dropping frames rather than queueing them. Video travels as QUIC datagrams with Reed-Solomon parity; control, input and files use reliable QUIC streams. The viewer reassembles frames within an 8 ms reorder window, decodes on the GPU and renders with wgpu.
Read the full design in ARCHITECTURE.md and what's next in the roadmap.