Skip to main content

Mirror the center 16:9 of an ultrawide monitor to a TV — lightweight DXGI capture, no OBS.

Project description

uwmirror

CI PyPI Python License: MIT

Mirror the center 16:9 of an ultrawide monitor to a TV — without running OBS.

You have a 32:9 ultrawide (5120×1440 or 3840×1080) and a TV on the same PC in Extend mode. You want the middle of the ultrawide — where the game or content actually is — fullscreen on the TV, with nothing else running. That's the whole tool: a borderless window on the TV showing a live center crop of the ultrawide, captured with the same zero-copy DXGI Desktop Duplication API OBS uses, in a single lightweight Python process (~150 MB RAM, a few percent of one core).

(demo GIF coming soon)

Windows only. Desktop Duplication is a Windows API; this tool is Windows 10/11 by design.

Install

No Python? Download uwmirror.exe from the latest release — a single self-contained file with the system-tray icon built in. Double-click it (no console window), or point Task Scheduler at it for autostart.

With Python (3.10+):

pipx install "uwmirror[tray]"      # or: uvx "uwmirror[tray]", or: pip install "uwmirror[tray]"

The [tray] extra adds the system-tray icon (pystray + Pillow). Plain uwmirror works too — you just control it with the global hotkeys instead of a tray. All dependencies are pure wheels (dxcam, pygame-ce, numpy).

Quickstart

uwmirror diagnose   # see both display enumerations and what auto-detection picks
uwmirror            # start mirroring

With two displays in Extend mode that's usually all you need: the widest display becomes the capture source and the other display the mirror target. If detection guesses wrong, pin the indices:

uwmirror --source 0 --target 1

Note: --source is a dxcam output index and --target is a pygame display index — two independent numbering schemes that don't necessarily match. uwmirror diagnose shows both.

Press Esc or Q in the mirror window to quit (or Ctrl+C in the console).

Options

Every flag can also be set in %APPDATA%\uwmirror\config.toml (same names, _ or - both accepted). Precedence: CLI flag > config file > auto-detection.

Flag Default Description
--source N auto dxcam output index of the monitor to capture
--target N auto pygame display index of the TV
--fps N 60 capture/present rate (30 halves CPU use)
--scale smooth|fast smooth downscale filter when the crop is larger than the TV
--cursor / --no-cursor on overlay the mouse cursor (capture omits it)
--topmost / --no-topmost on keep the mirror above other windows, without stealing focus
--windowed off small framed window instead of fullscreen (debugging)
--hotkeys / --no-hotkeys on global pause/blank/quit hotkeys
--tray / --no-tray on system-tray icon (needs the [tray] extra)
--pause-hotkey SPEC ctrl+alt+p freeze the mirror on the last frame
--blank-hotkey SPEC ctrl+alt+b black out the TV (privacy)
--quit-hotkey SPEC ctrl+alt+q quit the mirror
--backend dxcam|dxcam-cpp dxcam capture library (pip install uwmirror[cpp] for the C++ fork)
--config PATH %APPDATA%\uwmirror\config.toml config file location
--log-level LEVEL info debug, info, warning, error

Example config.toml (run uwmirror diagnose to get one pre-filled):

source = 0
target = 1
fps = 30
pause-hotkey = "ctrl+alt+f9"

Controls

The mirror window on the TV deliberately never takes focus, so you drive it from the tray icon (right-click → Pause / Blank / Quit, with live checkmarks) and from global hotkeys:

Keys Action
Ctrl+Alt+P (global) pause/resume — freezes the last frame
Ctrl+Alt+B (global) blank/unblank — black screen
Ctrl+Alt+Q (global) quit
Space / B / Esc / Q (mirror window focused) same, as a local fallback

Global hotkeys use RegisterHotKey — no admin rights, no keyboard hooks. If a chord is taken by another app, uwmirror logs a warning and keeps running; pick a different chord with --pause-hotkey/--blank-hotkey/--quit-hotkey.

Start automatically at logon

Point Task Scheduler at uwmirror.exe, or at uwmirrorw (the console-less entry point of a pip install) — see docs/autostart.md for the one-line schtasks command and the reason for the startup delay.

Limitations

Inherent to Desktop Duplication (OBS has the same ones):

  • No real cursor in the capture — uwmirror draws its own arrow overlay; it's always an arrow (no I-beam/hand shapes).
  • DRM-protected content (Netflix and friends) shows as a black region.
  • HDR desktops look washed out on an SDR TV — run the desktop in SDR.
  • UAC prompts briefly freeze the mirror (secure desktop); it recovers by itself.
  • Exclusive-fullscreen games reset the capture device on entry/exit; uwmirror reinitializes within a couple of seconds. Borderless-windowed games avoid the hiccup entirely.

Why not just OBS?

An OBS fullscreen projector does the same job well. uwmirror is for when you don't want an OBS instance running for it: one process, no compositor, no UI, roughly a third of the RAM (~150 MB vs ~400 MB), and it starts from Task Scheduler in about a second. If you already run OBS for streaming, keep using OBS.

Development

See CONTRIBUTING.md. Short version:

py -3.12 -m venv .venv && .venv\Scripts\activate
pip install -e . --group dev
ruff check . && ruff format --check . && mypy && pytest

Real-capture integration tests (need a real desktop + GPU): pytest -m local_display --no-cov.

Building the standalone exe is documented in docs/build-exe.md.

License

MIT © Jonathan Allard

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

uwmirror-1.0.0.tar.gz (53.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

uwmirror-1.0.0-py3-none-any.whl (30.8 kB view details)

Uploaded Python 3

File details

Details for the file uwmirror-1.0.0.tar.gz.

File metadata

  • Download URL: uwmirror-1.0.0.tar.gz
  • Upload date:
  • Size: 53.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for uwmirror-1.0.0.tar.gz
Algorithm Hash digest
SHA256 93e1b2262f542f3ac789ee5994e7fb81563e26fb8af2327ff6714ec5c83fac50
MD5 e92424ad3b46c6d3a67900b855f368a6
BLAKE2b-256 b02047d977f7e3331c463a3b0916d45c79163eee3d99dc229c1c6ffd9aab279d

See more details on using hashes here.

Provenance

The following attestation bundles were made for uwmirror-1.0.0.tar.gz:

Publisher: release.yml on j-Allard-22/uwmirror

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file uwmirror-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: uwmirror-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 30.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for uwmirror-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bf6f49461e79c481b446532697062404b7c54c1860ce425eb8ff18a5bdd15e73
MD5 00b87bf2e95d07fb525b62f34090e764
BLAKE2b-256 17deb8b9c6b907f7ba6e5f319dbe055f33c08ea9cf9f1f00936ab5ab94382de8

See more details on using hashes here.

Provenance

The following attestation bundles were made for uwmirror-1.0.0-py3-none-any.whl:

Publisher: release.yml on j-Allard-22/uwmirror

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page