I Turned My Android Tablet Into a Real Second Monitor — Over a USB Cable
Every few months I’d end up working somewhere that wasn’t my desk, with a laptop, a tablet in the same bag, and the persistent feeling that…
I Turned My Android Tablet Into a Real Second Monitor — Over a USB Cable

Every few months I’d end up working somewhere that wasn’t my desk, with a laptop, a tablet in the same bag, and the persistent feeling that one of those two screens was being wasted.
The tools that promise to fix this mostly do one of two things. They mirror a display you already have, which is not a second monitor — it’s the same monitor, twice. Or they send your screen over Wi-Fi through a service you have to sign into, which means your desktop is now a video stream on someone else’s infrastructure.
I wanted the third thing: a genuine second display, over the cable already sitting between the two devices. So I built TethrLink.
What it actually does
Plug the tablet into the Linux PC, turn on USB tethering, open the app. A new monitor appears in GNOME’s display settings. You arrange it where you want, drag windows onto it, and they stay there — GNOME treats it as an ordinary output because, as far as it knows, it is one.
USB tethering is the quiet trick. It already gives the PC and the tablet a private network over the cable. No Wi-Fi, no router, no cloud, no account. The entire network path is 30 centimetres of USB.
As of 2.0 the tablet can also drive the pointer: tap to click, drag to drag, long-press to right-click, two-finger drag to scroll.
The pipeline
The PC asks Mutter’s ScreenCast D-Bus API to create a dedicated virtual monitor. Frames come out through PipeWire, GStreamer encodes them as H.264 on the GPU, and a length-prefixed TCP stream carries them down the cable. The tablet decodes with MediaCodec and renders fullscreen. A UDP broadcast handles discovery, so there’s no IP address to type in.

Simple to describe. Three parts of it were not simple to get right.
Lesson 1: you cannot trust an encoder until it encodes something
My first version picked a hardware encoder by checking whether the GStreamer element existed. This works on the machine you wrote it on and nowhere else.
An element can be installed, expose exactly the property you’re setting, and still fail to encode a single frame. Worse, the rate-control modes a driver offers are read from the GPU at runtime, so the same element behaves differently on two machines with the same distro.
So the selection became empirical. Candidates are tried hardware-first — NVENC, VA-API, QSV, then software x264 — and each one is accepted only after it genuinely encodes a frame at the real capture size. Whatever wins gets cached along with a fingerprint of the GStreamer install, so a driver upgrade quietly triggers a re-probe. Cold, that costs about 2 seconds; from cache, under half a second.
The general form of this lesson: capability detection that asks “does this exist?” is a guess. Capability detection that asks “did this just work?” is an answer.
Lesson 2: drop frames before the encoder, never after
When the link can’t keep up, something has to give. The instinct is to drop the frames that are backing up — which, if they’re already encoded, is a disaster.
H.264 frames reference each other. Discard an encoded frame and you’ve broken the decoder’s reference chain; everything after it is visibly corrupt until the next keyframe arrives. Discard a raw frame, before encoding, and the only consequence is a slightly lower frame rate that nobody notices.
So a leaky queue sits upstream of the encoder, and every stage downstream of it is lossless. Buffering is kept deliberately shallow — about four frames in flight, roughly 133ms at 30fps. A deeper queue would smooth out jitter, but it would pay for that with latency on every single frame, and interactive latency is the whole product.
Lesson 3: a still screen produces no frames at all
This one produced the strangest bug of the project. Everything worked, until the tablet showed something that wasn’t moving — then it appeared to freeze, and when motion resumed it came back late.
Mutter’s capture is damage-driven. If nothing on the display changes, nothing is captured, because there’s nothing new to capture. That is entirely correct behaviour and completely wrong for a video stream, which wants a heartbeat.
The fix is a compositor element running on its own clock downstream of capture, manufacturing a constant frame rate at zero added latency. Motion now resumes instantly. The honest cost, documented in the README: an idle session still runs the pipeline, about half a core.
Geometry, and why the pointer used to hit a wall

The capture size isn’t fixed. It’s derived:
height = min(your monitor's height, the tablet's height)
width = height × the tablet's aspect ratio
Three separate problems fall out of that one formula. GNOME only lets the pointer cross between monitors where they overlap vertically, so matching heights makes the entire shared edge crossable instead of leaving an invisible wall partway along it. Taking the aspect ratio from the tablet keeps the upscale uniform, so a 16:9 desktop doesn’t get stretched 11% onto a 16:10 panel. And it keeps the decode load affordable — a tablet’s native 2960×1848 at 30fps is around 164 megapixels per second, near the practical ceiling for a single H.264 stream.
Touch, and the thing absolute positioning cannot do
Version 2.0 sends pointer intent back up the same TCP connection, injected into the desktop through GNOME’s RemoteDesktop API. Gesture timings come from Android’s own ViewConfiguration, so a long-press feels like a long-press everywhere else on the tablet and honours the device’s accessibility settings.
It’s off by default. Something that can drive your desktop should be a thing you turn on, not a thing that happens because a cable got plugged in.
And there’s a limitation I can’t engineer away: every touch clicks. There is no hover. A finger on glass either is or isn’t touching; there’s no state that corresponds to “pointer is here, but not pressed”. So tooltips and hover-triggered menus won’t open from touch. That’s inherent to driving a pointer by absolute position, not a bug I’m going to fix later.
What it requires, and what I removed
H.264 and touch both require GNOME on Wayland. The virtual display uses Mutter’s ScreenCast API and input uses GNOME’s RemoteDesktop API, and neither has a cross-compositor equivalent — there is no standard for creating a virtual output, only for capturing existing ones. X11 sessions still work, but fall back to JPEG at the PC’s own resolution, video only. One client at a time.
One more thing worth saying out loud. An earlier version of the documentation quoted an end-to-end latency figure. When I tried to reproduce it under controlled conditions, I couldn’t. So I withdrew it rather than keep repeating a number I couldn’t stand behind, and the README now says exactly that. Benchmarks you can’t reproduce are marketing.
Try it
sudo snap install tethrlink
Downloads and the project page: https://princesavsaviya.github.io/TethrLink/
Source, the .deb build script, the Android APK, and a much more detailed write-up of the pipeline are on GitHub: https://github.com/princesavsaviya/TethrLink — GPLv3, both halves.
Audio forwarding is next, then keyboard input. If you run it on AMD or Intel graphics I’d like to hear how the encoder negotiation goes; I developed against NVENC and an Intel iGPU, and lesson one suggests I should assume nothing about the rest.
One ask before you go: the Android client is heading to Google Play, and closed testing needs real devices — different panels, different ROMs, different ideas of what a tethered subnet looks like. If you have an Android tablet and a Linux machine on GNOME/Wayland, sign up for the alpha at https://princesavsaviya.github.io/TethrLink/#alpha — it takes one field.
메타데이터
- post_id
- 962c4c2e9758
- slug
- i-turned-my-android-tablet-into-a-real-second-monitor-over-a-usb-cable-962c4c2e9758
- url
- https://medium.com/@princesavsaviya2023.learning/i-turned-my-android-tablet-into-a-real-second-monitor-over-a-usb-cable-962c4c2e9758
- canonical_url
- https://medium.com/@princesavsaviya2023.learning/i-turned-my-android-tablet-into-a-real-second-monitor-over-a-usb-cable-962c4c2e9758
- author_url
- https://medium.com/@princesavsaviya2023.learning
- status
- ok
- fetched_at
- 2026-08-25 22:42:27