Terminal UI for using an Android phone as a Linux webcam via scrcpy and v4l2loopback.
Probes your phone's real cameras (lenses, resolutions, fps, zoom ranges), streams the one you pick into a virtual webcam device, and supervises the stream — auto-restarting when Android evicts the camera (face unlock, camera app opened, phone rebooted).
android-cam-tui --doctor checks all of these and prints the install command
for your distro:
- Linux with
v4l2loopbackloaded scrcpy≥ 3.0,adb,v4l2-ctlon PATH- Phone: USB debugging enabled, plugged in, and authorized for this computer. The screen may be off and locked — camera capture works through the keyguard, and waking a locked phone hands the camera to face unlock
When something on that list is missing the setup screen shows the same report
instead of starting. c puts every command on the clipboard as a shell script —
the TUI holds mouse tracking, so they cannot be selected out of it — then fix it
in another terminal and press r to re-check. Problems that only degrade things
— an old scrcpy, no preview player — are one yellow line on the setup screen and
do not stop you.
c needs wl-copy (wayland) or xclip (x11); without one, --doctor prints
the same thing to stdout.
Camera mirroring needs Android 12 (API 31) — scrcpy's --video-source=camera
doesn't work below it. android-cam-tui covers those phones with a companion
app instead: a foreground service, vendored into this binary, that opens the
camera itself and pipes H.264 to a socket for ffmpeg to decode into the sink.
It has no activity, so the phone is never woken or unlocked to run it. Source:
officialdad/android-cam.
--doctor detects a phone below API 31 on its own and adds three checks:
ffmpeg on PATH, the app installed, and the app holding camera permission. The
APK ships inside this binary and is written out to
~/.cache/android-cam-tui/android-cam.apk the moment an old phone shows up, so
the install fix is just adb install -r -g ~/.cache/android-cam-tui/android-cam.apk
— c puts it on the clipboard like any other fix line. -g grants every runtime
permission the app asks for, which here is only the camera; the ROMs that ignore
the flag need a manual
adb shell pm grant com.officialdad.androidcam android.permission.CAMERA, which
is its own check. All three drop to warnings when a modern phone is attached too —
they still describe the old handset, which you can still select. Below Android 8
they are not raised at all: the service is launched with
am start-foreground-service, which does not exist there, so that phone is a hard
block with no install that fixes it.
The APK reports its real cameras — ids, facing, sizes, frame rates, zoom
ceiling, sensor orientation — over dumpsys, so the picker shows what the
phone actually has: camera selection, real sizes and fps options, a real zoom
range, and the rotate option pre-selected from the sensor's orientation.
Camera, size, fps, bitrate and digital zoom ride the service's start intent;
rotation and aspect crop happen host-side in ffmpeg. There is no torch, and an
APK too old to answer the probe falls back to fixed 1280x720@30 requests.
curl -fsSL https://raw.githubusercontent.com/officialdad/android-cam-tui/main/install.sh | shDownloads the latest release for your architecture, verifies its checksum,
installs it to ~/.local/bin (override with PREFIX=), adds that directory to
your shell's PATH if it is missing, and finishes by reporting any runtime
dependency you still need.
Arch and derivatives can use the AUR package instead:
paru -S android-cam-tui-binOr take the tarball by hand from the
latest release —
linux-x64 and linux-arm64, each with a .sha256 beside it, no Bun needed at
runtime. From source instead (needs Bun ≥ 1.3):
git clone https://github.com/officialdad/android-cam-tui
cd android-cam-tui && bun install
bun startbun run build rebuilds the same binary into dist/.
android-cam-tui # setup screen
android-cam-tui --start # skip setup, stream the last-used configFrom source, bun start and bun start --start.
Setup screen: a device row, then three columns — camera, resolution, and output
(fps/zoom/bitrate/rotate/aspect/buffer/sink), all probed live. The same menu on
both backends; only buffer is scrcpy-specific. Tab/Shift-Tab move
between fields, 1-3 jump to a column, ↑↓ pick from a list, ←→ adjust,
/ filters a list (Esc clears), Enter starts. Under 100 columns wide it
shows one section at a time.
Rates marked hs are Android high-speed capture (--camera-high-speed), which
is where 120/240 fps live — they only appear on the resolutions that support
them, so pick the resolution first. Actual output can fall below the requested
rate in low light, since auto-exposure lengthens the exposure time.
rotate maps to scrcpy's --capture-orientation, so it rotates what reaches the
v4l2 device (not just a preview window) — use it when the phone sits in a mount.
aspect is a centered crop to 16:9, 4:3 or 1:1 (scrcpy's --crop; an ffmpeg
crop on the companion backend), applied before the rotation on both.
w on the setup screen promotes the selected USB device to WiFi: it runs
adb tcpip 5555, reads the phone's WLAN address, connects, and re-probes. The
cable can then be unplugged. Every scrcpy/adb call is scoped with -s <serial>,
so a USB and a wireless device can be attached at once without "more than one
device" errors.
w again on a wireless device goes back: adb usb restarts adbd on USB and
adb disconnect drops the TCP link, so the phone stops listening on 5555.
Plug the cable in first — adb usb travels over the very connection it
closes, so with no cable the phone drops off entirely and you have to replug
(or reboot it) to get it back. Equivalent by hand:
adb -s 192.168.1.42:5555 usb
adb disconnect 192.168.1.42:5555Until you do that (or the phone reboots), port 5555 stays open on your LAN —
anyone on the network who is already adb-authorised on that phone can connect.
Wireless adds jitter — raise buffer (--v4l2-buffer, in ms) if the feed stutters.
z cycle zoom presets, l cycle camera, t toggle the camera torch (fill
light — scrcpy backend only, the companion app has no torch), p open a
preview window, r restart, s back to setup, q quit.
Every camera parameter is fixed when the capture session opens, so zoom, torch and camera changes restart the stream — the sink drops for about a second.
p shells out to whichever of ffplay, mpv or vlc is installed and points
it at the sink, since scrcpy itself runs with --no-window. The preview window
is detached: close it yourself, it survives stream restarts.
The stream is supervised. When it dies, the exit code and scrcpy's own last
error line are logged, then it restarts with an exponential backoff, giving up
after 6 attempts so a config the phone rejects can't respawn forever. r clears
the count and retries. A stream that stayed up over a minute before dying starts
from a fresh budget, so a phone that locks once an hour never exhausts it.
Quitting — q, Ctrl+C, or a crash — kills scrcpy with it, so the camera and the
v4l2 sink are released rather than left busy for the next run.
To check the UI without a phone attached, bun run demo renders the setup
screen against fixture cameras. That is also what the GIF above records —
vhs docs/demo.tape regenerates it (needs vhs,
ttyd and ffmpeg).
Last-used config persists at ~/.config/android-cam-tui/config.json.
MIT
