mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2026-04-22 23:57:20 +08:00
xiaomi/cs2: fix TCP keepalive to match official Mi Home app
Based on PCAP analysis of official Mi Home app traffic, the keepalive mechanism was incorrect: Before (broken): - Sent PING every 5s only when receiving data - Responded to PING with PONG After (fixed): - Send PING every 1 second independently via dedicated goroutine - Don't respond to PING with PONG (official app doesn't either) - Both sides send PING bidirectionally as heartbeats The official app sends 199 PING messages and 0 PONG messages in a typical session. This fix matches that behavior. Fixes connection resets after prolonged streaming sessions with Xiaomi cameras using the CS2 P2P protocol.
This commit is contained in:
+38
-9
@@ -24,8 +24,16 @@ func Dial(host, transport string) (*Conn, error) {
|
||||
isTCP: isTCP,
|
||||
rawCh0: make(chan []byte, 10),
|
||||
rawCh2: make(chan []byte, 100),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
go c.worker()
|
||||
|
||||
// For TCP connections, start independent keepalive goroutine
|
||||
// Official Mi Home app sends PING every 1 second bidirectionally
|
||||
if isTCP {
|
||||
go c.keepalive()
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@@ -38,6 +46,7 @@ type Conn struct {
|
||||
seqCh3 uint16
|
||||
rawCh0 chan []byte
|
||||
rawCh2 chan []byte
|
||||
done chan struct{} // signals connection close to keepalive goroutine
|
||||
|
||||
cmdMu sync.Mutex
|
||||
cmdAck func()
|
||||
@@ -54,6 +63,7 @@ const (
|
||||
msgDrwAck = 0xD1
|
||||
msgPing = 0xE0
|
||||
msgPong = 0xE1
|
||||
msgAlive = 0xF0 // Camera heartbeat/alive signal
|
||||
msgClose = 0xF1
|
||||
)
|
||||
|
||||
@@ -102,17 +112,38 @@ func handshake(host, transport string) (net.Conn, error) {
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// keepalive sends PING every 1 second for TCP connections.
|
||||
// Based on PCAP analysis of official Mi Home app: both sides send PING bidirectionally,
|
||||
// neither responds with PONG. This keeps the connection alive.
|
||||
func (c *Conn) keepalive() {
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
ping := []byte{magic, msgPing, 0, 0}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.done:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if _, err := c.conn.Write(ping); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) worker() {
|
||||
defer func() {
|
||||
close(c.rawCh0)
|
||||
close(c.rawCh2)
|
||||
close(c.done) // signal keepalive goroutine to stop
|
||||
}()
|
||||
|
||||
chAck := make([]uint16, 4) // only for UDP
|
||||
buf := make([]byte, 1200)
|
||||
var ch2WaitSize int
|
||||
var ch2WaitData []byte
|
||||
var keepaliveTS time.Time
|
||||
|
||||
for {
|
||||
n, err := c.conn.Read(buf)
|
||||
@@ -125,13 +156,7 @@ func (c *Conn) worker() {
|
||||
case msgDrw:
|
||||
ch := buf[5]
|
||||
|
||||
if c.isTCP {
|
||||
// For TCP we should using ping/pong.
|
||||
if now := time.Now(); now.After(keepaliveTS) {
|
||||
_, _ = c.conn.Write([]byte{magic, msgPing, 0, 0})
|
||||
keepaliveTS = now.Add(5 * time.Second)
|
||||
}
|
||||
} else {
|
||||
if !c.isTCP {
|
||||
// For UDP we should using ack.
|
||||
seqHI := buf[6]
|
||||
seqLO := buf[7]
|
||||
@@ -179,7 +204,11 @@ func (c *Conn) worker() {
|
||||
}
|
||||
|
||||
case msgPing:
|
||||
_, _ = c.conn.Write([]byte{magic, msgPong, 0, 0})
|
||||
// Official Mi Home app: both sides send PING, neither responds with PONG
|
||||
// Just acknowledge receipt, don't send PONG
|
||||
continue
|
||||
case msgAlive:
|
||||
// Some cameras may send 0xF0 as heartbeat - just ignore like PING
|
||||
continue
|
||||
case msgPong, msgP2PRdyUDP, msgP2PRdyTCP, msgClose:
|
||||
continue // skip it
|
||||
|
||||
Reference in New Issue
Block a user