An enhanced fork of QtScrcpy v3.3.3 focused on FPS game usability — with bounded mouse movement, dual-touch recentering, a flexible multi-eye system, and stable audio streaming.
All changes are client-side only. The scrcpy-server on the Android device is unchanged — no root, no APK installation, no device modification required.
The original QtScrcpy maps mouse movement to a touch drag starting from startPos. That drag can travel across the entire screen with no bounds.
The problem: When the in-game UI changes suddenly (death screen, skill activation, new match), the internal touch position — which has drifted far from its starting point — may land on an interactive element in the new UI. This freezes mouse-look until the user manually resets by toggling the switch key, moving the mouse to an empty area, and toggling back.
This fork solves it by confining the touch drag to a configurable rectangle around startPos, with smooth automatic recentering when the boundary is reached.
┌─────────────── Phone Screen (0.05 – 0.95) ───────────────┐
│ │
│ ┌──── Local Bounds ────┐ │
│ │ │ │
│ │ · startPos │ ← touch stays here │
│ │ │ │
│ └──────────────────────┘ │
│ maxOffsetX ←─────→ │
│ │
│ [Kill Feed] [HP Bar] [Minimap] [Weapon Slots] │
│ │
│ After UI change, touch is still safely inside bounds │
│ instead of sitting on a random button │
└───────────────────────────────────────────────────────────┘
| Feature | Description |
|---|---|
| Bounded Movement | Confines touch drag to a rectangle around startPos |
| Recenter Delay | Time-based gap control during single-touch recenter |
| Dual-Touch Handoff | Zero-gap recenter using two alternating fingers |
| Extra Eyes | Multiple hold-to-activate anchors (Free Look, wheels, etc.) |
| Audio Fix | Stutter-free sound via jitter buffer and thread safety |
| Per-Pointer Dedup Fix | Critical multi-touch bug fix |
Add maxOffsetX and maxOffsetY to your mouseMoveMap:
"mouseMoveMap": {
"startPos": { "x": 0.463, "y": 0.424 },
"speedRatioX": 5,
"speedRatioY": 5,
"maxOffsetX": 0.08,
"maxOffsetY": 0.10
}maxOffsetX: 0.08→ touch can move ±8% of screen width fromstartPos.xmaxOffsetY: 0.10→ touch can move ±10% of screen height fromstartPos.y- Set both to
0or omit them → original unbounded behavior
Note: A larger boundary area reduces the frequency of recentering resets, which decreases system load and ensures smoother continuous movement.
Unlike hard-clipping (which cuts input and loses data), Soft-Bounding delivers your full mouse movement to the game first, then recenters afterward:
- Mouse delta is fully added to the internal touch position
- Physical clamping at screen edges (0.05–0.95); overflow is preserved
- Touch MOVE is sent to the device immediately (zero input loss)
- After sending: if position is outside local bounds → recenter
The game always receives your exact input. Recentering is invisible housekeeping.
Controls the pause between lifting the old touch and placing a new one during single-touch recenter. This delay is tied to the number of frames your phone renders per second; for example, 16 is the exact duration of a single frame when the device outputs a stable 60fps.
"recenterDelayMs": 16| Value | Effect |
|---|---|
0 |
Instant. Fastest response. May cause minor jitter on fast flicks |
16 |
Recommended. One frame at 60fps. Best balance |
30 |
Very safe. May feel slightly sluggish |
Only applies when
dualTouchModeis"none". Dual-touch modes ignore this value.
Instead of a single touch lifting and dropping (which creates a gap in input), this feature uses two alternating fingers. As you reach the boundary, a new touch pointer is placed at the center before the old one is lifted, ensuring uninterrupted camera movement without any dead zones.
"dualTouchMode": "clean"| Mode | Behavior |
|---|---|
"none" |
Single-touch recenter with recenterDelayMs (default) |
"clean" |
New finger DOWN, then old finger UP — same event |
"delayed" |
New finger DOWN now, old finger UP on next mouse event |
"overlap" |
New finger DOWN now, old finger MOVE+UP on next mouse event |
Why 3 modes? Touch event processing varies across different phones and game engines. Some games might ignore inputs or experience ghost touches with certain modes, so providing multiple options ensures compatibility. In practice, if supported, the three modes feel nearly identical because consecutive mouse events are ~1ms apart.
Note: If a game completely rejects all Dual-Touch modes (e.g., stopping camera movement entirely when the second finger drops), you must set this to "none" and configure a suitable recenterDelayMs instead.
State safety: mouseMoveStopTouch() acts as a garbage collector — when exiting mapped mode, all active and pending touches are cleaned up to prevent ghost touches.
Replaces the old single smallEyes with an array of hold-to-activate anchors:
"extraEyes": [
{
"key": "Key_Alt",
"pos": { "x": 0.785, "y": 0.31 }
},
{
"key": "Key_4",
"pos": { "x": 0.653, "y": 0.936 },
"maxOffsetX": 0.05,
"maxOffsetY": 0.05
}
]- Hold-to-activate — active while key is held; on release returns to
startPos - No reset/recenter ever — uses Hard-Clamping (movement stops at boundary) to prevent camera teleport in absolute-position mechanics like Free Look
- Unbounded by default —
maxOffsetX/Yat0or omitted → full screen (0.05–0.95) - Bounded if specified — movement clamped within area around
eye.pos
| Example | Bounds | Use Case |
|---|---|---|
Key_Alt |
None (full screen) | Free Look — needs wide rotation |
Key_4 |
0.05 × 0.05 |
Grenade/item wheel — precise selection |
The original sndcpy integration suffered from constant stuttering due to:
- Cross-thread writes (unsafe
QIODevice::write()from wrong thread) - No jitter buffer (socket → audio device directly → constant underruns)
- Deadlock on Stop Audio (
BlockingQueuedConnection)
Fixed architecture:
Socket (bursty) → QByteArray jitter buffer → QTimer drain (5ms) → Audio Device
↑
Pre-buffer ~100ms before playback starts
Buffer size: 200ms (38400 bytes)
Result: continuous stutter-free audio with safe start/stop.
Critical bug fix. The original code used a single global static QPoint for MOVE deduplication across all touch pointers. This caused packet spam or dropped events when multiple touches were active simultaneously (e.g., WASD movement + mouse look).
Fixed by replacing with QMap<int, QPoint> keyed by touch pointer ID.
| Field | Type | Default | Description |
|---|---|---|---|
startPos |
{x, y} | — | Center position for mouse-to-touch mapping |
speedRatioX |
float | — | Sensitivity divisor for X axis |
speedRatioY |
float | — | Sensitivity divisor for Y axis |
maxOffsetX |
float | 0 |
Horizontal bound radius (0 = unbounded) |
maxOffsetY |
float | 0 |
Vertical bound radius (0 = unbounded) |
recenterDelayMs |
int | 0 |
Delay in ms for single-touch recenter |
dualTouchMode |
string | "none" |
none / clean / delayed / overlap |
extraEyes |
array | [] |
Array of Eye objects (replaces smallEyes) |
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
key |
string | ✅ | — | Qt key code for hold-to-activate |
pos |
{x, y} | ✅ | — | Anchor position |
maxOffsetX |
float | ❌ | 0 |
Horizontal bound (0 = full screen) |
maxOffsetY |
float | ❌ | 0 |
Vertical bound (0 = full screen) |
{
"switchKey": "Key_QuoteLeft",
"mouseMoveMap": {
"type": "KMT_MOUSE_MOVE",
"startPos": { "x": 0.463, "y": 0.424 },
"speedRatioX": 5,
"speedRatioY": 5,
"maxOffsetX": 0.08,
"maxOffsetY": 0.10,
"recenterDelayMs": 16,
"dualTouchMode": "clean",
"extraEyes": [
{
"key": "Key_Alt",
"pos": { "x": 0.785, "y": 0.31 }
},
{
"key": "Key_4",
"pos": { "x": 0.653, "y": 0.936 },
"maxOffsetX": 0.05,
"maxOffsetY": 0.05
}
]
},
"keyMapNodes": [
"... your key bindings ..."
],
"width": 1920,
"height": 876
}| Setting | Recommended | Why |
|---|---|---|
recenterDelayMs |
16 |
One frame at 60fps |
dualTouchMode |
"clean" |
Safest zero-gap option |
speedRatioX/Y |
4 – 6 |
Moderate (see warning below) |
| Free Look eye | No bounds | Needs full rotation |
| Wheel eye | Small bounds (0.05) |
Precise clamped selection |
Place startPos in a screen area that stays empty across all UI states:
- Avoid screen center — death screens, popups appear there
- Good zone: right-of-center, slightly above middle (e.g.,
x: 0.65, y: 0.40) - Enable Pointer Location in Android Developer Options to visualize touches
Do not use very high speedRatio values as a workaround for the drift problem.
High speedRatio divides mouse deltas (which are integers from the OS) by a large number. Small movements round to zero, and larger movements produce visible jumping instead of smooth motion (quantization). This is a fundamental limitation — not a bug.
Keep speedRatio moderate (4–6) and adjust in-game sensitivity to complement it.
- All new JSON fields are optional. Existing key mapping files work without modification.
- Old
smallEyesJSON is recognized by the web editor and auto-converted toextraEyes[0].
A modified version of ScrcpyKeyMapper is included in the ScrcpyKeyMapper/ folder with full support for:
- Visual boundary rectangles (drag-to-resize)
- Independent Eye nodes with key binding
- Right-click to toggle boundaries
- All new
mouseMoveMapfields in property panel switchMaphidden from irrelevant node types
In the release package, use ScrcpyKeyMapper.lnk to launch it locally (requires Python).
The source code is in the QtScrcpy-project/ folder.
- Qt 5.12+ (tested with Qt 5.15.2)
- MSVC 2019 (Windows)
- Open
QtScrcpy-project/CMakeLists.txtin Qt Creator - Select kit: Desktop Qt 5.15.2 MSVC2019 64-bit
- Build Release (Ctrl+Shift+B)
- Output appears in
output/x64/Release/
For Linux build instructions, see the original project.
| File | Changes |
|---|---|
keymap.h |
maxOffsetX, maxOffsetY, recenterDelayMs, dualTouchMode, ExtraEye struct |
keymap.cpp |
Parsing for all new fields and extraEyes array |
inputconvertgame.h |
activeEyeIndex, secondaryTouchActive, PendingTouch, process flag |
inputconvertgame.cpp |
Soft-Bounding, dual-touch, extraEyes, per-pointer dedup, cleanup |
audiooutput.cpp |
Jitter buffer, timer drain, pre-buffer, thread safety, deadlock fix |
| File | Changes |
|---|---|
MouseMoveNode.js |
Boundary rect, new fields, removed smallEyes visual |
EyeNode.js |
New file — independent eye node |
NodeManager.js |
EyeNode registration |
ConfigManager.js |
Import/export extraEyes, backward compatibility |
index.html |
UI additions |
If this project helped you, consider supporting its development:
This is a fork of QtScrcpy by Barry, which is based on scrcpy by Genymobile.
The web key mapping editor is based on ScrcpyKeyMapper by w4po.
Licensed under the Apache License 2.0 — same as the original projects.