dahua-p2p
v1.0.0
Published
Dahua/Intelbras (Easy4ip, Mibo Smart) P2P tunnel and RTSP client for Node.js
Maintainers
Readme
dahua-p2p
Unofficial TypeScript P2P tunnel and RTSP client for Dahua and Intelbras (Mibo Smart) cameras — usable as a library or CLI
⚠️ Disclaimer: This project is not affiliated with, endorsed by, or associated with Zhejiang Dahua Technology Co., Ltd. or Intelbras S.A. in any way. Dahua, Easy4ip, DMSS, Intelbras and Mibo are trademarks of their respective owners. Use of these names and logos is solely for identification purposes. This is an independent, community-driven project developed through reverse engineering of the P2P protocol.
This project is intended for personal and research use only. It must not be used for commercial purposes, sold, or otherwise financially exploited.
Users connect only to their own devices, using their own device serial and credentials. No credentials are stored or transmitted by this library beyond what is necessary to reach the device on behalf of the user — no impersonation of Dahua, Intelbras or their services is intended.
Note to Dahua and Intelbras employees: If this repository raises any concerns regarding intellectual property, terms of service, or any other matter, please reach out via GitHub Issues or directly to @ludufre. I will promptly take down or modify the repository upon request — no legal action necessary.
Maintainers
| Maintainer | GitHub | Social | LinkedIn | | ---------------------- | ------------------------------------- | --------------------------------- | ------------------------------------------------------------------ | | Luan Freitas (ludufre) | ludufre | @ludufre | Luan Freitas |
Overview
Reach Dahua and Intelbras (Mibo Smart) cameras through the Easy4ip cloud relay, with no app, VPN or port forwarding. It works behind 4G and carrier-grade NAT.
The protocol comes from khoanguyen-3fc/dh-p2p (handshake and crypto) and blackjid/dahua-p2p (session behaviour). It adds a reliable PTCP layer. The relay loses and reorders datagrams, so the receiver only acknowledges contiguous bytes, parks early datagrams until the gap is filled, and drops duplicates. The device then retransmits whatever is missing. The PoC implementations above sum every received body into the ACK, and that stalls or corrupts the stream.
- Pure Node.js and TypeScript, with no native dependencies.
- Tunnel: any device port (RTSP 554, HTTP 80, 37777) is carried as a stream, or exposed on a local TCP port.
- RTSP client: Digest auth, video track only, and H.264/H.265 RTP depacketized into Annex B access units.
- Photo mode: open RTSP, take the first keyframe, TEARDOWN. The tunnel is reused between photos.
- Byte accounting: traffic is counted on the wire, IPv4/UDP headers included, which is what a metered link bills.
Only the device serial and the device user/password are needed. The RTSP login needs the credentials; the tunnel itself does not.
Install
npm install dahua-p2p # library
npx dahua-p2p --help # CLICLI
# Expose the camera's RTSP on 127.0.0.1:1554 (then: ffplay rtsp://admin:[email protected]:1554/cam/realmonitor?channel=1&subtype=1)
dahua-p2p tunnel <SERIAL> -l 1554
# 10 photos, one every 5 s, each with its wire cost; the first one saved as JPEG (needs ffmpeg)
dahua-p2p snapshot <SERIAL> -u admin -p <pass> -n 10 -i 5 -o photo.jpg
# Stability test: live video for 10 minutes, fails on a 5 s stall
dahua-p2p soak <SERIAL> -u admin -p <pass> -m 10Library
import { openTunnel, openRtsp, captureKeyframe } from 'dahua-p2p';
const tunnel = await openTunnel({ serial: '<SERIAL>' });
// Live frames
const rtsp = await openRtsp(tunnel, { user: 'admin', pass: '…', channel: 1, subtype: 1 });
rtsp.on('frame', f => console.log(f.codec, f.keyframe, f.data.length));
// One photo (the tunnel serializes realms: it waits for any other RTSP session to end)
const { frame, bytes } = await captureKeyframe(tunnel, { user: 'admin', pass: '…' });
console.log(tunnel.stats); // udpBytesIn/Out, PTCP counters
tunnel.close();Measured (Intelbras iM4-C, sub stream 640×480 H.264, relay, 2026-09-30)
| | | |---|---| | Handshake | ~0.5 s (p90 0.55 s) | | Live, 10 × 30 s | 10/10 intact streams. Each session saw 12–143 datagrams arrive out of order, and all of them were reordered | | Photo | ~0.8 s, 52–111 KB on the wire (keyframe 32–68 KB, plus video in flight until TEARDOWN) | | Idle tunnel | a heartbeat every 5 s, under 2 MB/month |
One relay session at a time
A device accepts one relay session at a time. It also keeps a closed session for about 15–20 s, until its heartbeats time out. Neither a realm DISC nor closing the socket frees it sooner. A second tunnel opened in that window, or while another client such as the vendor app holds the relay, never gets the agent's "device joined" confirmation.
openTunnel handles this. It waits 3 s for that confirmation (it normally arrives in about 100 ms), then retries every 4 s for up to busyRetryMs (35 s by default). If the device is still busy after that, it throws DeviceBusy. A tunnel reopened back-to-back is measured at ~22 s; a fresh one takes ~0.5 s.
Keep one tunnel per device and reuse it (RTSP sessions and photos come and go inside it). Close it only when you are really done, and remember that while it is open the vendor app may not get the relay.
Limitations
- Relay mode only. Direct P2P (inverted STUN) is not implemented, because it does not work behind CGNAT anyway.
- Devices that require channel authentication (HTTP 403 on
p2p-channel) throwDeviceAuthRequired. - The tunnel runs one realm (TCP connection) at a time. Devices can multiplex several, but serialising keeps RTSP negotiation reliable.
Tests
npm test # PTCP over a lossy/duplicating/reordering link, wrap-around, RTP depacketization