Skip to content

EmperorHeyman/Browser-Remote

Repository files navigation

Seamless browser remote

Control your projector's browser — or your entire PC — from your phone. No alt-tabbing, no extra hardware.

The Problem

We have 1 PC connected to 3 monitors and a projector. My girlfriend watches TV (YouTube, Netflix, etc.) on the projector through a browser while I game on the other monitors. Whenever an ad appeared or she wanted to switch shows, I had to alt-tab out of my game to help her navigate. That got old fast.

The Solution

A mobile-first web app that runs on the same PC and lets anyone on the local network control the projector's browser — or the entire desktop — from their phone. No app install needed — just scan a QR code or open a URL.

How It Works

Phone (browser) ──WiFi/HTTPS──▸ FastAPI Server ──CDP──▸ Brave Browser (on projector)
                                      │
                                      └──ctypes──▸ Windows OS cursor (PC mode)

The server talks to Brave via Chrome DevTools Protocol (CDP) using Playwright. All input is injected via CDP — the active window focus is never stolen, so gaming is uninterrupted. In PC mode, input bypasses the browser entirely and controls the OS cursor via Windows ctypes.

Why HTTPS / SSL Certificates

The server runs over HTTPS with self-signed certificates (key.pem + cert.pem). This is required, not optional:

  • Gyroscope access: Modern browsers (Chrome, Safari, Firefox) block the DeviceOrientationEvent and DeviceMotionEvent APIs on insecure origins. Without HTTPS, the gyro "Magic Remote" mode simply won't work — the browser will silently refuse to provide sensor data.
  • Safari specifically: Safari is the strictest — it won't even prompt for sensor permission unless the page is served over HTTPS. On plain HTTP, the gyro sub-mode is completely dead.
  • WebSocket upgrade: wss:// (secure WebSocket) is required for real-time mirror and gyro streaming when served over HTTPS. The frontend auto-detects the protocol and upgrades accordingly.

Generating Certificates

# Generate self-signed cert (run once in the project directory)
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=projector-remote"

On first connect, your phone will show a certificate warning — accept it once and you're good.


Two Operating Modes

Browser Mode (default)

Controls the projector's browser via CDP. All clicks, scrolls, and keypresses are injected into the browser DOM — your game's window focus is never touched.

  • Mirror: live stream of the browser tab via CDP Page.startScreencast
  • Click/scroll/hover: mapped to browser viewport coordinates via Playwright
  • Keyboard: injected into the active tab (arrow keys, Enter, Escape, media keys)
  • Fake cursor: a DOM-injected SVG pointer with click ripple animations (trackpad mode)
  • Navigation: open URLs, switch tabs, go back/forward, reload

PC Mode

Controls the entire Windows desktop. The mirror shows a real-time screenshot of a selected monitor, and all input goes to the OS cursor.

  • Mirror: live desktop capture via mss (Python screen capture library), streaming as JPEG over WebSocket
  • Click: moves the OS cursor to the tapped position and sends a left-click via mouse_event
  • Scroll: OS-level MOUSEEVENTF_WHEEL / MOUSEEVENTF_HWHEEL at the cursor position
  • Gyro: moves the real OS cursor, not a DOM element
  • Monitor switching: pick which display to mirror and control (each monitor is captured individually)

Toggle between modes with the 🖥️ Browser / ⚠️ PC Mode button in the top bar.


Features (Phone UI)

The phone interface has three main tabs: Mirror, Remote, and Browse.

Mirror Tab

Live view of what's on screen. Primary way to see and interact with content.

Feature Description
Live mirroring WebSocket stream — CDP screencast in Browser mode, mss desktop capture in PC mode
Tap-to-click Tap anywhere on the mirrored image to click that exact position
Swipe to scroll Swipe up/down to scroll the page at the touch position
Pinch-to-zoom 1x–5x zoom to read small text or target tiny UI elements
Zoom badge Shows current zoom level (top-right corner)
Pan When zoomed in, drag to pan around the page
Reset zoom One-tap button to snap back to 1x
Quick controls Esc, Backspace, Play/Pause, Fullscreen, Mute, Back, Forward
Monitor picker (PC mode only) Switch which display to mirror and control
Fallback polling If WebSocket drops, falls back to screenshot polling until reconnected

Remote Tab

Four sub-modes for different control styles:

D-Pad

Classic directional pad — up/down/left/right arrows + OK (Enter). Below it:

  • Media buttons: Play/Pause, Fullscreen, Mute
  • Text input field for typing search queries
  • Esc, Backspace shortcuts

Combo

Split screen — trackpad on the top half, D-pad + media controls on the bottom. Best of both worlds.

Trackpad

Full-screen relative trackpad:

  • Drag to move a DOM-injected fake cursor (Browser mode) or the OS cursor (PC mode)
  • Tap to click at the current cursor position (with ripple animation in Browser mode)
  • Velocity-based pointer acceleration — slow movements are precise (1.0x), fast flicks scale up to 3.5x. Covers a full 1080p screen in one swipe.
  • Media and shortcut buttons below the trackpad

Gyro ("Magic Remote")

Point your phone at the screen like a Wii Remote or LG Magic Remote:

  • Tap to wake — first tap activates gyro tracking, your phone tilt starts moving the cursor
  • Tap to click — while aiming, tap the trigger button to click at the cursor position
  • Auto-sleep — after 4 seconds of inactivity, gyro goes back to sleep to save battery; tap to wake again
  • DeviceOrientationEvent API — reads pitch (beta) and yaw (alpha) from the phone's gyroscope
  • EMA low-pass filter (α = 0.6) — smooths out hand tremor and sensor jitter
  • Asymmetric sensitivity — horizontal (40 px/°) and vertical (25 px/°) tuned for natural wrist mechanics
  • WebSocket transport — low-latency cursor streaming (not HTTP polling)
  • Portrait orientation lock — prevents auto-rotate from scrambling sensor axes mid-use
  • Haptic boundary feedback — phone vibrates when cursor hits screen edges
  • Boundary clamping — prevents cursor from going out of bounds
  • Fullscreen prompt — goes fullscreen on first tap for immersive use
  • Works in both Browser mode (DOM cursor) and PC mode (real OS cursor)

Browse Tab

Quick access to content without needing the mirror:

  • Quick Sites — one-tap cards for YouTube TV, Netflix, Oneplay (configurable in config.py)
  • URL bar — navigate to any URL manually
  • Tab Manager — lists all open browser tabs, tap to switch between them
  • Active site is highlighted

Top Bar

Always visible at the top of the screen:

  • Status dot — green when connected, red when disconnected
  • Page title — shows the active tab's title
  • Mode toggle — switch between Browser and PC mode
  • Connect — force reconnect to the browser CDP
  • Reload — reload the active browser tab

Standby Overlay

When no browser is running on the server:

  • Full-screen overlay with a "Tap to Start" button
  • Remotely launches the browser from the phone — no need to touch the PC

General UX

Feature Description
Haptic feedback Vibration on every button, tap, mode switch, gyro trigger, and screen edge hit
Toast notifications Brief popups for actions ("Connected!", "Switched to Display 2", etc.)
Auto-reconnect Exponential backoff WebSocket reconnection. Phone can sleep and auto-resume
Dark theme Optimized for use in a dark room next to a projector
No install Pure web — works in any mobile browser (Safari, Chrome, Firefox)

Desktop Launcher (PyQt6)

A native Windows GUI to configure and run everything from a single window.

Setup Page

  • Browser selector — auto-detects Brave, Chrome, Edge installations
  • Browser path — manual override for the executable path
  • Profile picker — use the browser's real profile (keeps logins/cookies) or an isolated one
  • Default URL — what opens when the browser launches (default: YouTube TV)
  • CDP port — Chrome DevTools Protocol port (default: 9222)
  • Server port — FastAPI server port (default: 5000)
  • Browser Display — which monitor to fullscreen the browser on (auto-detects all connected displays)
  • Start button — launches browser + server in one click

Active Page

Shown after the server starts:

  • QR code — scan with your phone to connect instantly (auto-installs qrcode package if missing)
  • Connection URL — displayed as text too, in case QR scanning isn't available
  • Status indicator — "Starting...", "Running | https://192.168.x.x:5000", or error state
  • Stop button — kills the server
  • Collapsible log drawer — real-time server output, toggle-able to save screen space

System Tray

  • Minimizes to tray instead of closing
  • Right-click menu: Show, Start Browser, Stop Server, Quit
  • Tray icon shows connection URL as tooltip

Other

  • Settings persist between sessions via %APPDATA%/ProjectorRemote/config.json
  • SSL auto-detection — if key.pem + cert.pem exist, QR/URL/status all show https://
  • PyInstaller build: pyinstaller projector_remote.spec to create a standalone .exe

Server API Reference

REST Endpoints

Method Path Description
GET / Serve the phone UI (index.html)
GET /api/status Connection state, viewport, PC mode, monitors
POST /api/connect Force reconnect to browser CDP
POST /api/start Launch the browser process remotely
POST /api/mode Toggle Browser/PC mode
GET /api/mode Get current mode
GET /api/monitors List all detected displays
POST /api/pc-monitor Select active monitor for PC mode
GET /api/pc-monitor Get active monitor info
GET /api/tabs List all open browser tabs
POST /api/tabs/{index} Switch to a specific tab
POST /api/click Click at relative (0-1) coordinates
POST /api/dblclick Double-click at relative coordinates
POST /api/hover Move mouse to relative coordinates
POST /api/scroll Scroll at a position with deltaX/deltaY
POST /api/cursor/show Inject/show the fake cursor
POST /api/cursor/hide Remove the fake cursor
POST /api/cursor/move Move cursor by dx/dy delta
POST /api/cursor/set Set cursor to absolute position
POST /api/cursor/click Click at current cursor position
POST /api/cursor/scroll Scroll at current cursor position
GET /api/cursor/position Get cursor position
GET /api/screenshot.jpg Grab a JPEG screenshot (fallback)
POST /api/press/{key} Press a keyboard key
POST /api/type Type a text string
POST /api/navigate Navigate to a URL (reuses matching tabs)
GET /api/sites Get configured quick-launch sites
POST /api/go-back Browser back
POST /api/go-forward Browser forward
POST /api/reload Reload the active tab

WebSocket Endpoints

Path Description
/ws/screencast CDP-based browser tab streaming (Browser mode)
/ws/screencast-desktop mss-based desktop capture streaming (PC mode)
/ws/gyro Bidirectional gyro cursor — receives {dx, dy} deltas, sends back {x, y} position

Known Problems

Issue Details
Switching monitors in PC mode is rough When switching the active monitor, the mirror WebSocket disconnects and reconnects. There can be a brief black frame or stutter during the transition. The monitor picker UI works but the switchover isn't seamless.
Self-signed cert warning On first connect, phones show a "Not Secure" warning for the self-signed SSL certificate. You have to manually accept it once. Some browsers (especially Safari) may require navigating to the URL directly and accepting the cert before WebSocket connections work.
Single PC only The server uses ctypes.windll for mouse control and mss for desktop capture — Windows only. No Linux/macOS support.

Setup

Prerequisites

  • Windows 10/11 (monitor detection and cursor control use Win32 API)
  • Python 3.10+
  • Brave, Chrome, or Edge browser
  • Phone on the same WiFi network
  • OpenSSL (to generate certificates, if not already done)

Install

cd RemoteProjector
pip install -r requirements.txt
python -m playwright install chromium

Generate SSL Certificates

openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=projector-remote"

Run (GUI Launcher)

python launcher.py

Select your browser, monitor, and click Start. The launcher will:

  1. Open the browser in fullscreen on your chosen monitor
  2. Start the HTTPS server with SSL
  3. Show a QR code to scan with your phone

Run (Command Line)

# Option 1: PowerShell script
.\start.ps1

# Option 3: Manual
# 1. Start browser with CDP
"C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe" `
    --remote-debugging-port=9222 `
    --user-data-dir="%LOCALAPPDATA%\BraveSoftware\Brave-Browser\User Data" `
    --start-fullscreen `
    --window-position=6400,134 `
    https://www.youtube.com/tv

# 2. Start server (with SSL)
python -m uvicorn server:app --host 0.0.0.0 --port 5000 --ssl-keyfile key.pem --ssl-certfile cert.pem

Connect

Scan the QR code shown in the launcher, or open https://<your-pc-ip>:5000 on your phone. Accept the certificate warning on first visit.


Project Structure

RemoteProjector/
├── launcher.py             # PyQt6 desktop GUI launcher
├── server.py               # FastAPI backend (CDP + OS cursor bridge)
├── config.py               # Configuration (%APPDATA% persistence)
├── projector_remote.spec   # PyInstaller build spec
├── key.pem                 # SSL private key (self-signed)
├── cert.pem                # SSL certificate (self-signed)
├── static/
│   └── index.html          # Mobile-first web frontend (single file, ~1500 lines)
├── start.ps1               # PowerShell one-click launcher
├── requirements.txt        # Python dependencies
└── READNE.md                # This file

Configuration

Settings are stored in %APPDATA%/ProjectorRemote/config.json and managed via the GUI launcher:

Setting Default Description
brave_path Auto-detected Path to browser executable
cdp_port 9222 Chrome DevTools Protocol port
port 5000 HTTPS server port
user_data_dir Browser's AppData Profile directory (keeps logins)
projector_monitor 0 Which display to fullscreen the browser on
default_url https://www.youtube.com/tv URL opened when browser launches
sites YouTube, Netflix, Oneplay Quick-launch site shortcuts

Tech Stack

  • Backend: Python 3.10+, FastAPI, Playwright (CDP), uvicorn, SSL/TLS
  • Frontend: Vanilla HTML/CSS/JS (single file, no build step, no dependencies)
  • Browser Streaming: WebSocket + CDP Page.startScreencast (auto-reconnect with exponential backoff)
  • Desktop Streaming: mss (multi-monitor screenshots) + Pillow (JPEG encoding)
  • Gyroscope: DeviceOrientationEvent API → WebSocket → server-side cursor accumulation
  • OS Input: Windows ctypesSetCursorPos, mouse_event, GetCursorPos, EnumDisplayMonitors
  • Desktop UI: PyQt6 (stacked widget, system tray, QR code generation)
  • Packaging: PyInstaller

License

MIT

About

I have 1 PC connected to 3 monitors and a projector. My girlfriend watches TV (YouTube, Netflix, etc.) on the projector through a browser while I game on the other monitors. Whenever an ad appeared or she wanted to switch shows, I had to alt-tab out of my game to help her navigate. That got old fast. This is the solution.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors