Turn a phone into a LAN camera + motion-sensor source for Python — one line, no app to install.
Project description
phonesense
Turn a phone into a LAN camera + motion-sensor source for Python — one line, no app to install. The phone's browser captures the camera and motion sensors and pushes them over your local network; your Python code reads frames and sensor data with zero setup on the phone.
Built for STEM classrooms where students write Python/OpenCV against a live camera, but useful anywhere you want a quick wireless webcam.
Two ways to use it
1. In-process (recommended, same machine)
Start the server from inside your Python script and grab frames directly — no separate process, no HTTPS hop, no self-signed-cert workaround:
import phonesense, cv2, numpy as np
cam = phonesense.start() # server starts in a background thread
print("On your phone, open:", cam.phone_url) # scan the QR the CLI prints, or type the URL
while True:
jpeg = cam.jpeg # latest JPEG bytes, or None until the phone connects
if jpeg is not None:
frame = cv2.imdecode(np.frombuffer(jpeg, np.uint8), cv2.IMREAD_COLOR)
cv2.imshow("phone", frame)
if cv2.waitKey(1) == 27: # Esc to quit
break
cam.stop()
phonesense hands you JPEG bytes and depends on no imaging library — decode with
OpenCV (above), Pillow, or anything else. cam.jpeg never blocks: it returns the
latest frame's bytes, or None until the phone has sent one.
2. Standalone server
Run the server on its own and point any consumer at it:
uvx phonesense # or: pipx run phonesense
It prints the LAN URLs and a scannable QR code. Open the phone page, tap Start, and watch the dashboard.
phonesense --port 8080 --host 0.0.0.0 --no-qr --cert-dir ./certs
On the phone
- Open
https://<your-lan-ip>:8080/phone(scan the printed QR to skip typing). - Tap through the one-time certificate warning (the cert is self-signed).
- Tap Start — the dashboard at
https://<your-lan-ip>:8080/shows live video. Enable sensors streams motion data.
No phone handy? Open
/phonein a second browser tab on the computer to use the computer's webcam instead.
OpenCV: snapshot vs stream
phonesense exposes each data type two ways — a live stream (follow every
new value) and a snapshot (one current value per request).
For same-machine use, the in-process API beats every HTTP route:
cam.jpeg (raw bytes), cam.sensors (dict) have no
HTTPS hop, no cert workaround, and no buffering. The HTTP endpoints below are
the off-machine (or non-Python) paths.
Camera — /camera/stream vs /camera
/camera/stream (MJPEG) |
/camera (single JPEG) |
|
|---|---|---|
| Model | continuous push | one request → one frame (poll) |
| OpenCV | cv2.VideoCapture(url) + loop .read() |
requests.get + cv2.imdecode |
| Throughput | high (one open connection) | low (a request per frame) |
| Latency | low, but FFmpeg may buffer a few frames | always the freshest single frame |
| Cert | needs OPENCV_FFMPEG_CAPTURE_OPTIONS=tls_verify;0 |
handled in Python (requests(..., verify=...)) |
| Use for | live video processing | thumbnails, periodic checks, curl/debug |
Continuous MJPEG into OpenCV (the off-machine one-liner):
OPENCV_FFMPEG_CAPTURE_OPTIONS=tls_verify;0 \
python -c "import cv2; c=cv2.VideoCapture('https://<ip>:8080/camera/stream'); print(c.read()[0], c.read()[1].shape)"
Single frame, decoded yourself (no FFmpeg, control your own TLS):
import requests, numpy as np, cv2
r = requests.get('https://<ip>:8080/camera', verify=False)
frame = cv2.imdecode(np.frombuffer(r.content, np.uint8), cv2.IMREAD_COLOR)
Sensors — /sensors/stream vs /sensors
/sensors/stream (SSE) |
/sensors (single JSON) |
|
|---|---|---|
| Model | continuous push (Server-Sent Events) | one request → latest reading (poll) |
| Consume | SSE client, or read data: lines |
requests.get(...).json() |
| Best for | smooth live charts, event-driven loops | one-off "what's the current tilt?" |
import requests
print(requests.get('https://<ip>:8080/sensors', verify=False).json()) # snapshot
A fuller OpenCV reachability check is in
examples/opencv_check.py.
Examples
Runnable scripts in examples/:
| File | Shows |
|---|---|
opencv_import.py |
in-process — phonesense.start() + cam.jpeg decode loop (recommended, same machine) |
opencv_url.py |
off-machine — cv2.VideoCapture('https://<ip>:8080/camera/stream') loop |
opencv_check.py |
diagnostic — confirms the feed is OpenCV-readable, with a manual-MJPEG fallback and a troubleshooting checklist |
The Camera handle
phonesense.start(port=8080, host="0.0.0.0", cert_dir=None, qr=True) returns a
Camera:
| Member | Kind | Returns / does |
|---|---|---|
jpeg |
property | latest frame as raw JPEG bytes, or None (non-blocking) |
sensors |
property | latest sensor dict, or None |
is_streaming |
property | True if a frame arrived in the last ~2s |
base_url, phone_url, dashboard_url, ingest_url, camera_url, camera_stream_url, sensors_url, sensors_stream_url, info_url, status_url, qr_url |
properties | the route URLs |
lan_ip, host, port |
attributes | URL components |
stop() |
method | stop the server, join the thread |
with ... as cam: |
context manager | stop() on exit |
phonesense depends on no imaging library — it transports JPEG bytes and
leaves decoding to you. Decode cam.jpeg with OpenCV
(cv2.imdecode(np.frombuffer(cam.jpeg, np.uint8), cv2.IMREAD_COLOR)), Pillow, or
anything else.
Routes
| Route | Purpose |
|---|---|
/ |
dashboard |
/phone |
phone capture page |
/ingest |
phone upload (WebSocket: JPEG frames + sensor JSON) |
/camera |
latest single JPEG |
/camera/stream |
live MJPEG (the cv2.VideoCapture URL) |
/sensors |
latest sensor reading (JSON) |
/sensors/stream |
live sensor stream (SSE) |
/info, /status |
dashboard connection info + liveness |
/qr.svg |
QR code for the phone URL |
Scheme: /X = latest single value, /X/stream = live stream, /ingest = the
phone's upload.
Architecture
The phone's browser captures JPEG frames + sensor JSON and pushes them over one
WebSocket (/ingest). A latest-value hub fans each new value out to every
consumer: frames to /camera/stream (MJPEG) and the in-process API, sensors to
/sensors/stream (SSE). Only one phone streams at a time — a new connection
takes over the previous one, so two phones never mix into one feed.
HTTPS is required because browsers only expose the camera over a secure origin.
The cert is self-signed, generated in pure Python (no system openssl) and
cached in a per-user data dir so it isn't regenerated each run.
Future: WebRTC
The current transport (WebSocket upload + MJPEG/SSE out) is simple, low-latency
on a LAN, and keeps the one-line OpenCV ingestion. For HD / high-FPS, or to drop
the certificate tap entirely, a WebRTC ingest path can publish into the same hub
without touching /camera/stream, the in-process API, or any consumer — the
boundary is already isolated in the upload handler.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file phonesense-0.1.0.tar.gz.
File metadata
- Download URL: phonesense-0.1.0.tar.gz
- Upload date:
- Size: 20.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8ee97586501188c3a0b7445e3b9e2cba1b4b342e66a05e8f280142e03962b811
|
|
| MD5 |
6cd4c934d219713a2251693448790ddb
|
|
| BLAKE2b-256 |
03454409b1b13214d0841fa6a1bf5b8f3b3ae622f14ec94870156cb6106bf20f
|
Provenance
The following attestation bundles were made for phonesense-0.1.0.tar.gz:
Publisher:
release.yml on snappyxo/phonesense
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
phonesense-0.1.0.tar.gz -
Subject digest:
8ee97586501188c3a0b7445e3b9e2cba1b4b342e66a05e8f280142e03962b811 - Sigstore transparency entry: 2002665653
- Sigstore integration time:
-
Permalink:
snappyxo/phonesense@afdbb194dff9e014fa703c9bddd08f911e7a6cc7 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/snappyxo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@afdbb194dff9e014fa703c9bddd08f911e7a6cc7 -
Trigger Event:
push
-
Statement type:
File details
Details for the file phonesense-0.1.0-py3-none-any.whl.
File metadata
- Download URL: phonesense-0.1.0-py3-none-any.whl
- Upload date:
- Size: 23.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ed66b386d4062ab1a79c1245d462b32cdedbae2d442d0886d6350cccdf3ba34f
|
|
| MD5 |
0af82821a5ad54198573133df66688ac
|
|
| BLAKE2b-256 |
87fc54bac74b703d5027eaf1f8bf5a86f2a81e74011d9b46ab7b2c3c96410d52
|
Provenance
The following attestation bundles were made for phonesense-0.1.0-py3-none-any.whl:
Publisher:
release.yml on snappyxo/phonesense
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
phonesense-0.1.0-py3-none-any.whl -
Subject digest:
ed66b386d4062ab1a79c1245d462b32cdedbae2d442d0886d6350cccdf3ba34f - Sigstore transparency entry: 2002665746
- Sigstore integration time:
-
Permalink:
snappyxo/phonesense@afdbb194dff9e014fa703c9bddd08f911e7a6cc7 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/snappyxo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@afdbb194dff9e014fa703c9bddd08f911e7a6cc7 -
Trigger Event:
push
-
Statement type: