Mirror the center 16:9 of an ultrawide monitor to a TV — lightweight DXGI capture, no OBS.
Project description
uwmirror
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:
--sourceis a dxcam output index and--targetis a pygame display index — two independent numbering schemes that don't necessarily match.uwmirror diagnoseshows 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
93e1b2262f542f3ac789ee5994e7fb81563e26fb8af2327ff6714ec5c83fac50
|
|
| MD5 |
e92424ad3b46c6d3a67900b855f368a6
|
|
| BLAKE2b-256 |
b02047d977f7e3331c463a3b0916d45c79163eee3d99dc229c1c6ffd9aab279d
|
Provenance
The following attestation bundles were made for uwmirror-1.0.0.tar.gz:
Publisher:
release.yml on j-Allard-22/uwmirror
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
uwmirror-1.0.0.tar.gz -
Subject digest:
93e1b2262f542f3ac789ee5994e7fb81563e26fb8af2327ff6714ec5c83fac50 - Sigstore transparency entry: 2148035343
- Sigstore integration time:
-
Permalink:
j-Allard-22/uwmirror@639f5c5bee9e03465d2717d9e4ca5d03b221abfc -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/j-Allard-22
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@639f5c5bee9e03465d2717d9e4ca5d03b221abfc -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bf6f49461e79c481b446532697062404b7c54c1860ce425eb8ff18a5bdd15e73
|
|
| MD5 |
00b87bf2e95d07fb525b62f34090e764
|
|
| BLAKE2b-256 |
17deb8b9c6b907f7ba6e5f319dbe055f33c08ea9cf9f1f00936ab5ab94382de8
|
Provenance
The following attestation bundles were made for uwmirror-1.0.0-py3-none-any.whl:
Publisher:
release.yml on j-Allard-22/uwmirror
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
uwmirror-1.0.0-py3-none-any.whl -
Subject digest:
bf6f49461e79c481b446532697062404b7c54c1860ce425eb8ff18a5bdd15e73 - Sigstore transparency entry: 2148035353
- Sigstore integration time:
-
Permalink:
j-Allard-22/uwmirror@639f5c5bee9e03465d2717d9e4ca5d03b221abfc -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/j-Allard-22
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@639f5c5bee9e03465d2717d9e4ca5d03b221abfc -
Trigger Event:
push
-
Statement type: