A Flutter app that turns a phone into a networked camera + sensor server — an ad-free, self-hosted alternative to IP Webcam, with an MCP endpoint so an AI can see through and control the camera.
Plain HTTP on :8080, TLS on :8443 (self-signed). Both expose the same API. The app is a menu that flows into per-feature screens; the server and camera run above the navigation, so every endpoint is live from launch regardless of the current screen.
| Endpoint | Returns |
|---|---|
/photo.jpg, /shot.jpg |
Full-sensor-resolution JPEG |
/video, /videofeed |
MJPEG stream (multipart/x-mixed-replace), ~4–5 fps / 640px |
/audio.wav |
Live 16 kHz mono PCM as streaming WAV |
| Endpoint | Effect |
|---|---|
/settings/zoom?set=2.5 |
Zoom level (clamped to device range) |
/settings/exposure?set=-1 |
Exposure offset (EV) |
/settings/focusmode?set=locked |
auto | locked |
/settings/exposuremode?set=locked |
auto | locked |
/focus?x=0.5&y=0.5 |
Focus point (omit params → auto) |
/enabletorch, /disabletorch, /settings/torch?set=on |
Torch |
/front, /back, /settings/camera?set=2 |
Camera select |
/settings/resolution?set=high |
low…max (re-inits) |
/sensors.json (accel/gyro/mag) · /battery.json · /gps.json ·
/status.json · / (HTML docs index)
POST /mcp — Model Context Protocol over Streamable HTTP (JSON-RPC 2.0).
Tools: capture_photo (returns an image block — the model sees the frame),
set_zoom, set_exposure, set_torch, switch_camera, set_resolution,
get_status, get_sensors, get_battery, get_gps.
Point any MCP client at http://<phone-ip>:8080/mcp.
- Connect an Android phone (USB or wireless debugging).
flutter run- Grant camera permission. Mic/location are requested lazily the first time
/audio.wav//gps.jsonare hit.
The app can't make the OS see a webcam by itself — bridge /video into a
virtual V4L2 device:
sudo modprobe v4l2loopback card_label="AIWebcam" exclusive_caps=1
ffmpeg -f mjpeg -i http://<phone-ip>:8080/video -vf format=yuv420p -f v4l2 /dev/videoNThen AIWebcam appears as a camera in Zoom/Meet/OBS/Chrome. (Cross-platform:
pull /video into OBS → Start Virtual Camera.) Quality is bounded by the
pure-Dart MJPEG encoder (~4–5 fps / 640px).
The certificate is generated on the device, not shipped with the app. On the
first start, lib/tls_identity.dart generates a 2048-bit RSA key and a
self-signed certificate in an isolate and caches both under the app's support
directory; every later start reuses them. So each install is a distinct
identity and the private key never leaves the phone.
That is deliberate. A key committed to the repository would be the same key on
every install, and anyone with a copy could impersonate or decrypt any other
user's camera — which is exactly what shipping assets/certs/key.pem used to
do here.
HTTPS is best-effort: if an identity can't be established for any reason, the
app logs it and keeps serving plain HTTP on :8080 rather than failing to
start.
The certificate is self-signed, so a client still has to accept it on first
connection — unavoidable for a device on a private network with no stable
hostname. curl -k works anywhere. For full verification, pull the cert off
the device once and pin it:
openssl s_client -connect <phone-ip>:8443 -showcerts </dev/null \
| openssl x509 -outform PEM > aiwebcam.pem
curl --cacert aiwebcam.pem https://<phone-ip>:8443/photo.jpgGive the phone a static DHCP reservation to keep that stable. To roll the
identity — if a device is lost, or a pinned cert should be invalidated — call
TlsIdentity.reset(); the next start generates a fresh one.
lib/
main.dart bootstrap + AppServices + lifecycle + entry menu
screens.dart Menu → Camera / Video / Audio / Telemetry / Docs
camera_engine.dart owns CameraController; stills, controls, MJPEG (isolate encode)
telemetry.dart sensors / battery / GPS
audio_streamer.dart mic → streaming WAV
webcam_server.dart dual HTTP/HTTPS + REST route registry
tls_identity.dart per-install self-signed cert, generated on first run
mcp_server.dart MCP (JSON-RPC) tools
- Stills vs. stream are exclusive. While
/videostreams, the camera can't alsotakePicture(), so/photo.jpgreturns the latest stream frame (preview-res) until the stream ends. With no stream,/photo.jpgis full-res. - No H.264/RTSP/WebRTC/ONVIF. Those need native hardware-encoder access (platform channels) — intentionally out of scope.
- Dedicated-phone model. Wakelock keeps the screen on; on iOS the app must stay foregrounded. Disable auto-lock.
Flutter · camera · shelf · network_info_plus · wakelock_plus ·
sensors_plus · battery_plus · geolocator · image · record.