tuya-ipc-p2p-sdk
Async Python SDK for the native P2P video of Tuya IPC cameras — the path the vendor app uses, spoken directly by this package. No vendor SDK, no external process and no cloud RTSP: it logs in, negotiates the session, decrypts the media and hands you JPEG frames.
⚠️ Unofficial. This project is not affiliated with, endorsed by or supported by Tuya. It targets the mobile API gateway and the P2P protocol their app uses. The vendor can change either side at any time and break this SDK without notice.
The official Tuya integrations stream these cameras over a cloud RTSP link, which several devices — pet feeders and other low-cost IPC hardware among them — do not offer. This SDK speaks the P2P protocol those devices do answer.
Status
Alpha — usable but not yet stable.
| Area | State |
|---|---|
| Email/password login against the mobile gateway | ✅ |
| Camera discovery (devices that answer the IPC config API) | ✅ |
| Signaling over MQTT, offer/answer, trickled candidates | ✅ |
| TCP relay transport (handshake, keepalive, tagged KCP frames) | ✅ |
| Channel-0 authentication and the start burst | ✅ |
| MJPEG frames, reassembled by chunk offset | ✅ |
| Supervised reconnects, stall detection, backoff | ✅ |
| Motion derived from frame sizes | ✅ |
| Audio (conversation 2) | ⚠️ received and discarded |
| The channel-0 control surface | ⚠️ decoded but not exposed |
| H264 cameras | ❌ untested — validated against MJPEG hardware only |
Regions other than us |
⚠️ declared, only us is exercised live |
| Public API stability | ❌ breaking changes still happen across 0.x |
See CHANGELOG.md for breaking changes between releases.
Install
pip install tuya-ipc-p2p-sdk
Or, with uv:
uv add tuya-ipc-p2p-sdk
What you need
- The account the camera belongs to: email, password and the
country code (the calling code —
1for the US,55for Brazil). - The region its gateway lives in (
usby default).
The device id and its local key are discovered from the account; nothing has to be copied out of the app by hand.
Usage
import asyncio
from tuya_ipc_p2p_sdk import TuyaIpcP2pClient
async def main() -> None:
async with TuyaIpcP2pClient("you@example.com", "secret", "55") as client:
cameras = await client.async_discover_cameras()
camera = cameras[0]
print(camera.name, camera.device_id)
stream = client.create_camera_stream(camera.device_id, camera.local_key)
await stream.async_start()
try:
frame = await stream.async_wait_for_frame(45)
if frame:
print(f"first JPEG: {len(frame)} bytes")
async for jpeg in stream.async_frames():
print(len(jpeg), "motion" if stream.motion_detected else "")
finally:
await stream.async_stop()
asyncio.run(main())
async_frames() starts with the most recent frame and then yields every new
one. Several consumers can read it at once; each gets its own shallow queue, so
a slow reader skips ahead rather than falling behind.
Runnable scripts live in examples/.
Cameras serve one client at a time
While a stream is running the vendor app cannot connect to that camera, and while the app holds it this SDK cannot. Bringing a cold session up takes several seconds — login, signaling, relay, the channel-0 handshake — so a consumer that wants video ready on demand keeps the stream running rather than starting it per request.
Motion
These cameras report no motion of their own. A JPEG is only as large as its content is complex, so how much each frame differs in size from the one before it is a direct measure of how much the scene changed — and it costs nothing, because the frames arrive either way.
Measured against a still scene on real hardware, consecutive frames differ by
0.2 % in daylight and 0.41 % at night, where sensor noise is higher. The typical
difference is tracked continuously rather than fixed, so the threshold follows
the camera from day into night, and motion_sensitivity sets how far above
typical a frame has to land. Two frames in a row have to exceed it: anything
really moving stays in shot longer than one frame, while the camera's own
exposure adjustments show up as a single frame that differs sharply from both
its neighbours.
It cannot tell a cat from the lights coming on. On a still scene at night — the noisiest case — the default settles at about one event every ten minutes; raise the sensitivity if that is too eager.
How it works
The native P2P path is WebRTC-shaped signaling with a custom media transport:
- Login and config. A signed, encrypted (
et=3) call to the mobile gateway logs in;m.ipc.v4.rtc.config.getreturns a server-coordinated session, the ICE servers and a relay token. Each fetch mints a fresh session — a camera does not answer an offer built from a stale one — so every reconnect refetches it. - Signaling. An SDP offer/answer is exchanged as JSON over MQTT, inside a
binary "2.2" envelope whose body is AES-ECB'd with the device local key. The
media line negotiates
AES/KCPrather than DTLS-SRTP. - Transport. Media flows over the TCP relay named in the offer: one connection multiplexes several KCP conversations — control on conversation 0, video on 1, audio on 2 — each frame authenticated by a per-frame HMAC tag.
- Media. After a channel-0 authentication the camera starts sending. The records are AES-128-CBC, and the video conversation reassembles into JPEG frames by chunk offset.
The ICE role matters. The device runs its connectivity checks as the
controlling agent even though the client is the offerer, and it waits for that
negotiation to conclude before committing media. A client that also claims the
controlling role produces a role conflict the device never resolves: it sends
the first frames and then tears the session down with close_reason=6 seconds
later. This SDK answers the checks as the controlled agent, which keeps
sessions up indefinitely. Media itself rides the relay, so no candidate pair has
to be nominated for video to flow.
The byte-level protocol reference lives in the companion Go client,
tuya-ipc-p2p (PROTOCOL.md).
This package is an independent Python implementation of the same protocol, and
its tests pin the shared vectors — signatures, key derivations, envelope bytes
and the channel-0 packet layout — against that reference.
Staying up
Each session ends on transport close, a device disconnect, or a stall (no frame
within the stall timeout). The supervisor then backs off, fetches a fresh config
and starts over, resetting the backoff after any session that actually streamed
and logging in again if the gateway rejects the session. Every session publishes
a disconnect on its way out, so the device releases it rather than refusing
the next offer.
Layout
src/tuya_ipc_p2p_sdk/
├── client.py the entry point: one account, its cameras, its streams
├── camera_stream.py keeps one camera streaming and fans its frames out
├── stream_session.py one session end to end
├── motion_detector.py motion read out of frame sizes
├── control.py channel-0 auth and the start burst
├── media.py channel-1 packet extraction
├── jpeg_reassembler.py JPEG frames rebuilt by chunk offset
├── crypto.py AES-CBC records, the AES-ECB envelope, PKCS#7
├── json_types.py the JSON boundary and its accessors
├── gateway/ request signing, encrypted bodies, login, config fetch
├── signaling/ SDP, the MQTT envelope, the signaling client, handshake signing
├── transport/ TCP relay framing, KCP, the controlled-role ICE responder
├── models/ the typed records the SDK hands back
└── exceptions/ the error hierarchy
Development
uv sync
./scripts/lint # ruff format, ruff check, mypy, pytest
The suite is network-free: a fake relay server speaks the handshake and media framing over loopback, and a fake broker stands in for MQTT, so the session tests exercise the real transport rather than a mock of it.
License
MIT — see LICENSE.
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 tuya_ipc_p2p_sdk-0.1.0.tar.gz.
File metadata
- Download URL: tuya_ipc_p2p_sdk-0.1.0.tar.gz
- Upload date:
- Size: 148.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6c89ba937697299f2723b6604be44927c76f32c67bb175ee17a9c93913618ada
|
|
| MD5 |
0ea859f3eb0d4a094320b6a5b9897f9a
|
|
| BLAKE2b-256 |
47f19138298612d616a8dd806b9a844729cea1251a4325e3b117027e48b1479a
|
File details
Details for the file tuya_ipc_p2p_sdk-0.1.0-py3-none-any.whl.
File metadata
- Download URL: tuya_ipc_p2p_sdk-0.1.0-py3-none-any.whl
- Upload date:
- Size: 64.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
78d53c9dc5b126eaf277bcb73ee9dac19fdcd1d36d16ee9fb801a86a8aba2c40
|
|
| MD5 |
16ac3a9385dfdd47f07dc228c9d47078
|
|
| BLAKE2b-256 |
ca33a3183c3d86f45496be6578f7c83cd042a034ca455706963323cee74bb77d
|