- Rust 64.4%
- Python 13.6%
- HTML 11.2%
- C 7.1%
- Shell 3.7%
Draft itch page copy (tagline + description, pay-what-you-want/suggested $3) and a step-by-step shipping checklist (itch + Stripe, GitHub Sponsors, optional GitHub push-mirror, README link fill). Code + copy done; the rest is account setup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A7yrHRq9FjgypppnjtDdpj |
||
|---|---|---|
| docs | ||
| packaging | ||
| rust | ||
| static | ||
| .gitignore | ||
| anycam.py | ||
| CLA.md | ||
| COMMERCIAL-LICENSING.md | ||
| LICENSE | ||
| pwcam.c | ||
| README.md | ||
AnyCam
Stream your phone's camera (any camera-enabled device with a browser) to your
Linux computer over the local network, and publish it as a PipeWire camera —
no kernel module, no v4l2loopback, no module signing, no MOK, no reboot. Pure
Python 3 stdlib + openssl for the web part; the virtual camera uses PipeWire +
GStreamer (preinstalled on most modern desktops).
✅ The virtual camera works in libwebrtc apps (browsers) now
AnyCam's camera node is produced by
pwcam— a small raw-libpipewiresource that negotiates SPA formats/buffers the way a real camera does, which is what libwebrtc requires. Verified in Firefox AND Vesktop (real pixels measured via/camtest). The oldgst-pipewiresinknode (which libwebrtc could never consume — full diagnosis indocs/VIRTUAL-CAMERA.md) remains only as a build-less fallback. Verified in both Firefox and Vesktop on 2026-07-05. Rewrite spec + results:docs/DEEP-REWRITE.md. Still 100% userspace: nov4l2loopback, no MOK, no kernel module, Secure Boot stays on.
Run
python3 anycam.py
Keep it running permanently
AnyCam ships as a systemd user service (user-level so it keeps access to your session's PipeWire):
# ~/.config/systemd/user/anycam.service (see repo history for the unit)
systemctl --user enable --now anycam.service # start now + at every login
systemctl --user status anycam.service # check on it
journalctl --user -u anycam.service -f # live logs
loginctl enable-linger $USER # optional: survive logout
Restart=always respawns it after any crash (and keeps retrying at boot until
the drive holding the app directory is mounted).
Then:
- On your phone (same Wi-Fi): open
https://<LAN-IP>:8443/— the exact address is printed in the terminal. Tap Advanced → Proceed on the one-time certificate warning, press Start Camera, allow camera access. - On the computer: open
http://localhost:8080/view— live view, no warnings (localhost is a secure context). - Test which apps can use the camera: open
http://localhost:8080/camtestin the app you care about. It measures the actual pixels received and shows a brightness reading — green = real image, red/0= black/no frames.
The virtual camera — what works
While running, AnyCam publishes a PipeWire camera node "AnyCam Phone Camera"
(media.class=Video/Source, media.role=Camera) via the bundled pwcam
helper (raw libpipewire-0.3, auto-compiled on first run). 100% userspace: no
/dev/video*, no kernel module, Secure Boot stays on.
| Consumer | Works? | Notes |
|---|---|---|
| Firefox (browser) | ✅ | verified: real pixels via /camtest (I420 negotiated) |
| OBS (PipeWire Video Capture source) | ✅ | nothing to configure |
| GNOME Camera / Snapshot | ✅ | portal-native |
GStreamer pipewiresrc, portal consumers |
✅ | 30fps, delivers cleanly |
| MJPEG consumers (VLC/ffplay, see below) | ✅ | separate HTTP path, always works |
| Vesktop (Electron/Discord, Chromium libwebrtc) | ✅ | verified: placeholder + camtest pass |
| Chrome / other Chromium | 🟡 | same capturer; may need chrome://flags PipeWire camera + a one-time portal permission click |
When no phone is streaming the node shows a "camera not connected" frame; a
phone stream takes over live (letterboxed to whatever the app negotiated).
Disable the node with --no-vcam.
Building pwcam happens automatically at startup when gcc, pkg-config
and the dev headers are present:
sudo apt install libpipewire-0.3-dev libswscale-dev libturbojpeg0-dev
Without them AnyCam falls back to the old gst-pipewiresink node, which works
for OBS/portal apps but delivers zero frames to libwebrtc (browsers,
Discord) — that incompatibility is diagnosed exhaustively in
docs/VIRTUAL-CAMERA.md.
Feeding apps that read MJPEG or /dev/video
The stream is also plain MJPEG on localhost — this path always works, independent of PipeWire:
| Consumer | How |
|---|---|
| VLC | vlc http://localhost:8080/stream.mjpg |
| ffplay | ffplay http://localhost:8080/stream.mjpg |
| OBS | Media/Browser Source with the URL above |
| latest still | GET http://localhost:8080/snapshot.jpg |
(Browsers and Discord don't need /dev/video* anymore — the pwcam PipeWire
node covers them. The historical v4l2loopback notes remain in
docs/VIRTUAL-CAMERA.md §"The v4l2loopback path" for
reference only.)
Why is the phone side HTTPS?
Mobile browsers disable getUserMedia on non-localhost http://. AnyCam
generates a self-signed cert on first run (cert/); you tap through the warning
once per phone. The desktop side stays plain HTTP on localhost (already secure).
The cert is generated to Apple's TLS rules (subjectAltName with the LAN IP, ≤825-day validity, serverAuth EKU) — without those, iPhones refuse the connection outright ("server unexpectedly dropped the connection") instead of offering the tap-through warning. If your machine's LAN IP changes, AnyCam notices the SAN mismatch at startup and regenerates the cert automatically (the phone then shows one fresh certificate warning — tap through again).
Options
python3 anycam.py --port 8443 --local-port 8080 [--no-vcam]
Endpoints
| Path (localhost:8080) | Purpose |
|---|---|
/view |
Desktop live viewer (WebSocket) |
/camtest |
Diagnostic: measures pixels an app actually receives |
/stream.mjpg |
MJPEG multipart stream |
/snapshot.jpg |
Latest single frame |
wss://…:8443/ws?role=pub |
Phone publishes here (HTTPS) |
Files
anycam.py— the whole server (stdlib only).pwcam.c— the raw-libpipewire virtual camera source (auto-compiled topwcamon first run; delete the binary to force a rebuild).static/—index.html(phone capture),view.html(desktop viewer),camtest.html(per-app camera diagnostic;?report=1POSTs results to/camtest/reportfor headless verification).cert/— auto-generated self-signed cert (delete to regenerate).placeholder.jpg— generated "camera not connected" frame (delete to regen).docs/— architecture, the virtual-camera diagnosis, and the rewrite spec.
Notes
- Trusted-LAN tool: anyone on the LAN who accepts the cert can view or publish.
- One phone publishes at a time (second publisher takes over); any number of viewers.
- Phone page has live controls: resolution (480p / 720p / 1080p), frame rate (20 / 30 / 60 fps — applied to the camera itself, not just the send loop), JPEG quality, camera pick + front/back flip, live preview with the actual capture size in the status line, landscape/rotation re-negotiation, a hardened screen wake-lock (re-acquired if the OS revokes it), and a Dim screen button (75% black veil against OLED burn-in — preview stays faintly visible; tap anywhere to undim).
- The camera node advertises I420/NV12/YUY2 at 1280×720, 1920×1080 and 640×480, each at 30 and 60 fps — apps negotiate what they want (verified up to 1080p60). Frames are letterboxed to whatever the consumer picked.
License & commercial use
AnyCam is free software under the GNU AGPL-3.0 (see LICENSE).
- Using AnyCam is always free — including commercially. Stream, record for YouTube/Twitch, take video calls, work on camera: no fee, no license, no attribution. The AGPL places no conditions on use.
- Forks and redistribution must stay AGPL-3.0 and keep the copyright notices.
- Want to embed AnyCam's code in a closed-source or proprietary product?
That needs a commercial license — see
COMMERCIAL-LICENSING.md.
Contributions are welcome under the Contributor License Agreement.
Support this project
AnyCam is pay-what-you-want. If it saved you the price of a webcam, consider chipping in:
- Pay-what-you-want download:
- Sponsor / tip: