From 44da81774cc37e8bffd0d55f2e32d18cab37742c Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 1 Jan 2026 08:32:39 +0300 Subject: [PATCH 1/6] Add recv/send counters to xiaomi source --- pkg/xiaomi/backchannel.go | 4 ++++ pkg/xiaomi/producer.go | 2 ++ 2 files changed, 6 insertions(+) diff --git a/pkg/xiaomi/backchannel.go b/pkg/xiaomi/backchannel.go index 0224a594..a6f57b81 100644 --- a/pkg/xiaomi/backchannel.go +++ b/pkg/xiaomi/backchannel.go @@ -31,6 +31,7 @@ func (p *Producer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv buf = append(buf, transcode(pkt.Payload)...) const size = 2 * 8000 * 0.040 // 16bit 40ms for len(buf) >= size { + p.Send += size _ = p.client.WriteAudio(miss.CodecPCM, buf[:size]) buf = buf[size:] } @@ -40,6 +41,7 @@ func (p *Producer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv buf = append(buf, pkt.Payload...) const size = 8000 * 0.040 // 8bit 40 ms for len(buf) >= size { + p.Send += size _ = p.client.WriteAudio(miss.CodecPCMA, buf[:size]) buf = buf[size:] } @@ -54,12 +56,14 @@ func (p *Producer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv } else { // convert two 20ms to one 40ms buf = opus.JoinFrames(buf, pkt.Payload) + p.Send += len(buf) _ = p.client.WriteAudio(miss.CodecOPUS, buf) buf = nil } } } else { sender.Handler = func(pkt *rtp.Packet) { + p.Send += len(pkt.Payload) _ = p.client.WriteAudio(miss.CodecOPUS, pkt.Payload) } } diff --git a/pkg/xiaomi/producer.go b/pkg/xiaomi/producer.go index 09ba7360..9abf66b0 100644 --- a/pkg/xiaomi/producer.go +++ b/pkg/xiaomi/producer.go @@ -164,6 +164,8 @@ func (p *Producer) Start() error { return err } + p.Recv += len(pkt.Payload) + // TODO: rewrite this var name string var pkt2 *core.Packet From e77210f916f61d5ae0d947c3111e039546d6b494 Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 1 Jan 2026 07:49:21 +0300 Subject: [PATCH 2/6] Improve support tutk vendor for xiaomi source --- pkg/xiaomi/miss/client.go | 2 +- pkg/xiaomi/producer.go | 4 +- pkg/xiaomi/tutk/README.md | 63 +++++++ pkg/xiaomi/tutk/conn.go | 326 +++++++---------------------------- pkg/xiaomi/tutk/crypto.go | 23 +-- pkg/xiaomi/tutk/proto.go | 196 +++++++++++++++++++++ pkg/xiaomi/tutk/proto_new.go | 191 ++++++++++++++++++++ pkg/xiaomi/tutk/proto_old.go | 153 ++++++++++++++++ 8 files changed, 682 insertions(+), 276 deletions(-) create mode 100644 pkg/xiaomi/tutk/README.md create mode 100644 pkg/xiaomi/tutk/proto.go create mode 100644 pkg/xiaomi/tutk/proto_new.go create mode 100644 pkg/xiaomi/tutk/proto_old.go diff --git a/pkg/xiaomi/miss/client.go b/pkg/xiaomi/miss/client.go index 470c0e0e..560f9f49 100644 --- a/pkg/xiaomi/miss/client.go +++ b/pkg/xiaomi/miss/client.go @@ -35,7 +35,7 @@ func Dial(rawURL string) (*Client, error) { case "cs2": c.conn, err = cs2.Dial(u.Host, query.Get("transport")) case "tutk": - c.conn, err = tutk.Dial(u.Host, query.Get("uid")) + c.conn, err = tutk.Dial(u.Host, query.Get("uid"), query.Get("model")) default: return nil, fmt.Errorf("miss: unsupported vendor %s", s) } diff --git a/pkg/xiaomi/producer.go b/pkg/xiaomi/producer.go index 9abf66b0..f2ce4eda 100644 --- a/pkg/xiaomi/producer.go +++ b/pkg/xiaomi/producer.go @@ -75,7 +75,7 @@ func Dial(rawURL string) (core.Producer, error) { } func probe(client *miss.Client, channel, quality, audio uint8) ([]*core.Media, error) { - _ = client.SetDeadline(time.Now().Add(core.ProbeTimeout)) + _ = client.SetDeadline(time.Now().Add(10 * time.Second)) if err := client.VideoStart(channel, quality, audio&1); err != nil { return nil, err @@ -158,7 +158,7 @@ func (p *Producer) Start() error { var audioTS uint32 for { - _ = p.client.SetDeadline(time.Now().Add(core.ConnDeadline)) + _ = p.client.SetDeadline(time.Now().Add(10 * time.Second)) pkt, err := p.client.ReadPacket() if err != nil { return err diff --git a/pkg/xiaomi/tutk/README.md b/pkg/xiaomi/tutk/README.md new file mode 100644 index 00000000..95773594 --- /dev/null +++ b/pkg/xiaomi/tutk/README.md @@ -0,0 +1,63 @@ +# TUTK + +The most terrible protocol I have ever had to work with. + +## Messages + +Ping from camera (24b). The shortest message. + +``` +off sample +0 0402 tutk magic +2 190a tutk version (120a, 190a...) +4 0800 msg size = len(b)-16 = 24-16 +6 0000 channel seq (always 0 for ping) +8 2804 msg type (2804 - ping from camera, 0804 - usual msg from camera) +10 1200 direction (12 - from camera, 21 - from client) +12 00000000 fixed +16 7ecc93c4 random +20 56c2561f random +``` + +Usual msg from camera (52b + msg data). + +``` +off sample +12 e6e8 same bytes b[20:22] +14 0000 channel (0, 1, 5) +16 0c00 fixed +18 0000 fixed +20 e6e839da random session id +24 66b0dc14 random session id +28 0070 command +30 0b00 version +32 0100 command seq +34 0000 ??? +36 00000000 ??? +40 00000000 ??? +44 e300 msg data size +46 0000 ??? +48 8f15a02f random msg id +52 ... msg data +``` + +Message with media from camera. + +``` +off sample +28 0c00 command +30 0b00 version +32 7700 command seq +34 0000 ??? data only for last message per pack (14/14) +36 0200 pack seq, don't know how packs used +38 0914 09/14 - message seq/messages per packs +40 01000000 fixed +42 0500 command 2 +44 3200 command 2 seq +46 4f00 chunks count per this frame +48 1b00 chunk seq, starts from 0 (wrong for last chunk) +50 0004 frame data size +52 c8f6 random msg id +54 01000000 previous frame seq, starts from 0 +58 02000000 current frame seq, starts from 1 +``` diff --git a/pkg/xiaomi/tutk/conn.go b/pkg/xiaomi/tutk/conn.go index f3005224..37489ffa 100644 --- a/pkg/xiaomi/tutk/conn.go +++ b/pkg/xiaomi/tutk/conn.go @@ -12,23 +12,33 @@ import ( "time" ) -func Dial(host, uid string) (*Conn, error) { +func Dial(host, uid, model string) (*Conn, error) { conn, err := net.ListenUDP("udp", nil) if err != nil { return nil, err } - c := &Conn{ - conn: conn, - addr: &net.UDPAddr{IP: net.ParseIP(host), Port: 32761}, - sid: genSID(), + addr, err := net.ResolveUDPAddr("udp", host) + if err != nil { + addr = &net.UDPAddr{IP: net.ParseIP(host), Port: 32761} } + c := &Conn{conn: conn, addr: addr, sid: genSID()} + if err = c.handshake([]byte(uid)); err != nil { _ = c.Close() return nil, err } + switch model { + case "isa.camera.df3": + c.msgCtrl = c.oldMsgCtrl + c.handleCh0 = c.oldHandlerCh0() + default: + c.msgCtrl = c.newMsgCtrl + c.handleCh0 = c.newHandlerCh0() + } + c.rawCmd = make(chan []byte, 10) c.rawPkt = make(chan []byte, 100) @@ -43,20 +53,32 @@ type Conn struct { sid []byte err error - seqCh0 uint16 - seqCmd uint16 rawCmd chan []byte rawPkt chan []byte cmdMu sync.Mutex cmdAck func() + + seqSendCh0 uint16 + seqSendCh1 uint16 + + seqSendCmd1 uint16 + seqSendCmd2 uint16 + seqSendCnt uint16 + + seqRecvPkt0 uint16 + seqRecvPkt1 uint16 + seqRecvCmd2 uint16 + + msgCtrl func(ctrlType uint16, ctrlData []byte) []byte + handleCh0 func(cmd []byte) int8 } func (c *Conn) handshake(uid []byte) (err error) { _ = c.SetDeadline(time.Now().Add(5 * time.Second)) if _, err = c.WriteAndWait( - c.msgLanSearch(uid, 1), // 01062100 + c.msgConnectByUID(uid, 1), func(_, res []byte) bool { return bytes.Index(res, uid) == 16 // 02061200 }, @@ -64,15 +86,14 @@ func (c *Conn) handshake(uid []byte) (err error) { return err } - if err = c.Write(c.msgLanSearch(uid, 2)); err != nil { + if err = c.Write(c.msgConnectByUID(uid, 2)); err != nil { return err } if _, err = c.WriteAndWait( - c.msgAvClientStartReq(), // 07042100 + 00000b00 + c.msgAvClientStart(), func(req, res []byte) bool { - mid := req[48:52] - return bytes.Index(res, mid) == 48 // 08041200 + 00140800 + return bytes.Index(res, req[48:52]) == 48 }, ); err != nil { return err @@ -90,135 +111,45 @@ func (c *Conn) worker() { }() buf := make([]byte, 1200) - var waitSeq uint16 - var waitSize uint32 - var waitData []byte for { - n, addr, err := c.conn.ReadFromUDP(buf) + n, _, err := c.ReadFromUDP(buf) if err != nil { c.err = fmt.Errorf("%s: %w", "tutk", err) return } + if c.handleMsg(buf[:n]) <= 0 { + if c.err != nil { + return + } + fmt.Printf("tutk: unknown msg: %x\n", buf[:n]) + } + } +} + +func (c *Conn) Write(buf []byte) error { + //log.Printf("-> %x", buf) + _, err := c.conn.WriteToUDP(TransCodePartial(nil, buf), c.addr) + return err +} + +func (c *Conn) ReadFromUDP(buf []byte) (n int, addr *net.UDPAddr, err error) { + for { + if n, addr, err = c.conn.ReadFromUDP(buf); err != nil { + return 0, nil, err + } + if string(addr.IP) != string(c.addr.IP) || n < 16 { continue // skip messages from another IP } - b := ReverseTransCodePartial(buf[:n]) - //log.Printf("<- %x", b) - - if b[0] != 0x04 || b[1] != 0x02 { - continue - } - - if len(b) == 24 { - _ = c.Write(msgAckPing(b)) - continue - } - - switch b[14] { - case 0: - switch string(b[28:30]) { - case "\x00\x12": - _ = c.Write(c.msgAckCh0Req0012(b)) - continue - - case "\x00\x70": - _ = c.Write(c.msgAckCh0Req0070(b)) - select { - case c.rawCmd <- b[52:]: - default: - } - continue - - case "\x00\x71": - if c.cmdAck != nil { - c.cmdAck() - } - continue - - case "\x01\x03": - seq := binary.LittleEndian.Uint16(b[40:]) - if seq != waitSeq { - waitSeq = 0 // data loss - continue - } - if seq == 0 { - waitSize = binary.LittleEndian.Uint32(b[36:]) + 32 - } - - waitData = append(waitData, b[52:]...) - if n := uint32(len(waitData)); n < waitSize { - waitSeq++ - continue - } else if n > waitSize { - waitSeq = 0 // data loss - continue - } - - // create a buffer for the header and collected data - packetData := make([]byte, waitSize) - // there's a header at the end - let's move it to the beginning - copy(packetData, waitData[waitSize-32:]) - copy(packetData[32:], waitData) - - select { - case c.rawPkt <- packetData: - default: - c.err = fmt.Errorf("%s: media queue is full", "tutk") - return - } - - waitSeq = 0 - waitData = waitData[:0] - continue - - case "\x01\x04": - waitSize2 := binary.LittleEndian.Uint32(b[36:]) - waitData2 := b[52:] - - if uint32(len(waitData2)) != waitSize2 { - continue // shouldn't happened for audio - } - - packetData := make([]byte, waitSize2) - copy(packetData, waitData2) - - select { - case c.rawPkt <- packetData: - default: - c.err = fmt.Errorf("%s: media queue is full", "tutk") - return - } - continue - } - case 1: - switch string(b[28:30]) { - case "\x00\x00": - _ = c.Write(msgAckCh1Req0000(b)) - continue - case "\x00\x07": - _ = c.Write(msgAckCh1Req0007(b)) - continue - } - case 5: - if len(b) == 48 { - _ = c.Write(msgAckCh5(b)) - continue - } - } - - fmt.Printf("%s: unknown msg: %x\n", "tutk", buf[:n]) + ReverseTransCodePartial(buf, buf[:n]) + //log.Printf("<- %x", buf[:n]) + return n, addr, nil } } -func (c *Conn) Write(req []byte) error { - //log.Printf("-> %x", req) - _, err := c.conn.WriteToUDP(TransCodePartial(req), c.addr) - return err -} - func (c *Conn) WriteAndWait(req []byte, ok func(req, res []byte) bool) ([]byte, error) { var t *time.Timer t = time.AfterFunc(1, func() { @@ -231,20 +162,14 @@ func (c *Conn) WriteAndWait(req []byte, ok func(req, res []byte) bool) ([]byte, buf := make([]byte, 1200) for { - n, addr, err := c.conn.ReadFromUDP(buf) + n, addr, err := c.ReadFromUDP(buf) if err != nil { return nil, err } - if string(addr.IP) != string(c.addr.IP) || n < 16 { - continue // skip messages from another IP - } - - res := ReverseTransCodePartial(buf[:n]) - //log.Printf("<- %x", b) - if ok(req, res) { + if ok(req, buf[:n]) { c.addr.Port = addr.Port - return res, nil + return buf[:n], nil } } } @@ -298,10 +223,10 @@ func (c *Conn) WriteCommand(cmd uint16, data []byte) error { timeout.Reset(1) } - req := c.msgAvSendIOCtrl(cmd, data) + msg := c.msgCtrl(cmd, data) for { - if err := c.Write(req); err != nil { + if err := c.WriteCh0(msg); err != nil { return err } <-timeout.C @@ -334,128 +259,3 @@ func genSID() []byte { b[4] = 0x0c return b } - -func (c *Conn) msgLanSearch(uid []byte, i byte) []byte { - const size = 68 // or 52 or 68 or 88 - b := make([]byte, size) - copy(b, "\x04\x02\x0f\x02") - b[4] = size - 16 - copy(b[8:], "\x01\x06\x21\x00") - copy(b[16:], uid) - copy(b[52:], "\x00\x03\x01\x02") // or 07000303 or 01010204 - copy(b[56:], c.sid[8:]) - b[64] = i - return b -} - -func (c *Conn) msg(size uint16) []byte { - b := make([]byte, size) - copy(b, "\x04\x02\x19\x0a") - binary.LittleEndian.PutUint16(b[4:], size-16) - binary.LittleEndian.PutUint16(b[6:], c.seqCh0) - c.seqCh0++ // start from 0 - copy(b[8:], "\x07\x04\x21\x00") - return b -} - -func (c *Conn) msgAvClientStartReq() []byte { - const size = 586 // or 586 or 598 - b := c.msg(size) - copy(b[12:], c.sid) - copy(b[28:], "\x00\x00\x08\x00") // or 00000400 or 00000b00 - binary.LittleEndian.PutUint16(b[44:], size-52) - binary.LittleEndian.PutUint32(b[48:], uint32(time.Now().UnixMilli())) - copy(b[size-16:], "\x04\x00\x00\x00\xfb\x07\x1f\x00") - return b -} - -func (c *Conn) msgAvSendIOCtrl(cmd uint16, msg []byte) []byte { - size := 52 + 4 + uint16(len(msg)) - b := c.msg(size) - copy(b[12:], c.sid) - copy(b[28:], "\x00\x70\x08\x00") // or 00700400 or 00700b00 - c.seqCmd++ // start from 1 - binary.LittleEndian.PutUint16(b[32:], c.seqCmd) - binary.LittleEndian.PutUint16(b[44:], size-52) - //_, _ = rand.Read(b[48:52]) // mid - binary.LittleEndian.PutUint32(b[48:], uint32(time.Now().UnixMilli())) - binary.LittleEndian.PutUint16(b[52:], cmd) - copy(b[56:], msg) - return b -} - -const version = 0x19 - -func msgAckPing(req []byte) []byte { - // <- [24] 0402120a 08000000 28041200 000000005b0d4202070aa8c0 - // -> [24] 04021a0a 08000000 27042100 000000005b0d4202070aa8c0 - req[2] = version - req[8] = 0x27 - req[10] = 0x21 - return req -} - -func msgAck(req []byte, size byte) []byte { - // xxxx??xx ??00xxxx 07xx21xx ... - req[2] = version - req[4] = size - 16 - req[5] = 0x00 - req[8] = 0x07 - req[10] = 0x21 - return req[:size] -} - -func (c *Conn) msgAckCh0Req0012(req []byte) []byte { - // <- [64] 0402120a 30000000 08041200 e6e8 0000 0c000000e6e839da66b0dc14 00120800000000000000000000000000 0c00 000000000000 020000000100000001000000 - // -> [72] 0402190a 38000300 07042100 e6e8 0000 0c000000e6e839da66b0dc14 00130b00000000000000000000000000 1400 000000000000 0200000001000000010000000000000000000000 - const size = 72 - req = append(req, 0, 0, 0, 0, 0, 0, 0, 0) - binary.LittleEndian.PutUint16(req[6:], c.seqCh0) // channel sequence - c.seqCh0++ - req[28] = 0x00 // command - req[29] = 0x13 - req[44] = size - 52 // data size - req[45] = 0x00 - return msgAck(req, size) -} - -func (c *Conn) msgAckCh0Req0070(req []byte) []byte { - // <- [104] 0402120a 58000300 08041200 e6e8 0000 0c000000e6e839da66b0dc14 00700800010000000000000000000000 3400 00007625a02f ... - // -> [ 52] 0402190a 24000400 07042100 e6e8 0000 0c000000e6e839da66b0dc14 00710800010000000000000000000000 0000 00007625a02f - binary.LittleEndian.PutUint16(req[6:], c.seqCh0) // channel sequence - c.seqCh0++ - req[28] = 0x00 // command - req[29] = 0x71 - req[44] = 0x00 // data size - req[45] = 0x00 - return msgAck(req, 52) -} - -func msgAckCh1Req0000(req []byte) []byte { - // <- [590] 0402120a 3e020100 08041200 e6e8 0100 0c000000e6e839da66b0dc14 00000800000000000000000000000000 1a02 0000d9c0001b ... - // -> [ 84] 0402190a 44000000 07042100 e6e8 0100 0c000000e6e839da66b0dc14 00140b00000000000000000000000000 2000 0000d9c0001b ... - const size = 84 - req[28] = 0x00 // command - req[29] = 0x14 - req[44] = size - 52 // data size - req[45] = 0x00 - copy(req[52:], req[len(req)-32:]) // size - return msgAck(req, size) -} - -func msgAckCh1Req0007(req []byte) []byte { - // <- [64] 0402120a 30000300 08041200 e6e8 0100 0c000000e6e839da66b0dc14 00070800000000000000000000000000 0c00 000001000000 000000006f1ea02f00000000 - // -> [56] 0402190a 28000200 07042100 e6e8 0100 0c000000e6e839da66b0dc14 010a0b00000000000000000000000000 0000 000001000000 00000000 - req[28] = 0x01 // command - req[29] = 0x0a - req[44] = 0x00 // data size - req[45] = 0x00 - return msgAck(req, 56) -} - -func msgAckCh5(req []byte) []byte { - // <- [48] 0402120a 20000200 08041200 e6e8 0500 0c000000e6e839da66b0dc14 5a97c2f1010500000000000000000000 00a0 0000 - // -> [48] 0402190a 20000200 07042100 e6e8 0500 0c000000e6e839da66b0dc14 5a97c2f1410500000000000000000000 00a0 0000 - req[32] = 0x41 - return msgAck(req, 48) -} diff --git a/pkg/xiaomi/tutk/crypto.go b/pkg/xiaomi/tutk/crypto.go index c98fc092..feeb31f7 100644 --- a/pkg/xiaomi/tutk/crypto.go +++ b/pkg/xiaomi/tutk/crypto.go @@ -1,7 +1,6 @@ package tutk import ( - "bytes" "encoding/binary" "math/bits" ) @@ -9,10 +8,12 @@ import ( // I'd like to say hello to Charlie. Your name is forever etched into the history of streaming software. const charlie = "Charlie is the designer of P2P!!" -func ReverseTransCodePartial(src []byte) []byte { +func ReverseTransCodePartial(dst, src []byte) []byte { n := len(src) tmp := make([]byte, n) - dst := bytes.Clone(src) + if len(dst) < n { + dst = make([]byte, n) + } src16 := src tmp16 := tmp @@ -24,7 +25,7 @@ func ReverseTransCodePartial(src []byte) []byte { binary.LittleEndian.PutUint32(tmp16[i:], bits.RotateLeft32(x, i+3)) } - swap(tmp16, dst16, 16) + swap(dst16, tmp16, 16) for i := 0; i != 16; i++ { tmp16[i] = dst16[i] ^ charlie[i] @@ -40,7 +41,7 @@ func ReverseTransCodePartial(src []byte) []byte { src16 = src16[16:] } - swap(src16, tmp16, n) + swap(tmp16, src16, n) for i := 0; i < n; i++ { dst16[i] = tmp16[i] ^ charlie[i] @@ -49,10 +50,12 @@ func ReverseTransCodePartial(src []byte) []byte { return dst } -func TransCodePartial(src []byte) []byte { +func TransCodePartial(dst, src []byte) []byte { n := len(src) tmp := make([]byte, n) - dst := bytes.Clone(src) + if len(dst) < n { + dst = make([]byte, n) + } src16 := src tmp16 := tmp @@ -68,7 +71,7 @@ func TransCodePartial(src []byte) []byte { dst16[i] = tmp16[i] ^ charlie[i] } - swap(dst16, tmp16, 16) + swap(tmp16, dst16, 16) for i := 0; i != 16; i += 4 { x := binary.LittleEndian.Uint32(tmp16[i:]) @@ -84,12 +87,12 @@ func TransCodePartial(src []byte) []byte { tmp16[i] = src16[i] ^ charlie[i] } - swap(tmp16, dst16, n) + swap(dst16, tmp16, n) return dst } -func swap(src, dst []byte, n int) { +func swap(dst, src []byte, n int) { switch n { case 2: _, _ = src[1], dst[1] diff --git a/pkg/xiaomi/tutk/proto.go b/pkg/xiaomi/tutk/proto.go new file mode 100644 index 00000000..86cf95af --- /dev/null +++ b/pkg/xiaomi/tutk/proto.go @@ -0,0 +1,196 @@ +package tutk + +import ( + "encoding/binary" + "time" +) + +func (c *Conn) WriteCh0(msg []byte) error { + binary.LittleEndian.PutUint16(msg[6:], c.seqSendCh0) + c.seqSendCh0++ + return c.Write(msg) +} + +func (c *Conn) WriteCh1(msg []byte) error { + binary.LittleEndian.PutUint16(msg[6:], c.seqSendCh1) + c.seqSendCh1++ + msg[14] = 1 // channel + return c.Write(msg) +} + +func (c *Conn) msgConnectByUID(uid []byte, i byte) []byte { + const size = 68 // or 52 or 68 or 88 + b := make([]byte, size) + copy(b, "\x04\x02\x19\x02") + b[4] = size - 16 + copy(b[8:], "\x01\x06\x21\x00") + copy(b[16:], uid) + copy(b[52:], "\x00\x03\x01\x02") // or 07000303 or 01010204 + copy(b[56:], c.sid[8:]) + b[64] = i // 1 or 2 + return b +} + +func (c *Conn) msgAvClientStart() []byte { + const size = 566 + 32 + msg := c.msg(size) + + cmd := msg[msgHhrSize:] + copy(cmd, "\x00\x00\x0b\x00") + binary.LittleEndian.PutUint16(cmd[16:], size-52) + //cmd[18] = 1 // ??? + binary.LittleEndian.PutUint32(cmd[20:], uint32(time.Now().UnixMilli())) + + // important values for some cameras (not for df3) + data := cmd[cmdHdrSize:] + copy(data, "Miss") + copy(data[257:], "client") + + // 0100000004000000fb071f000000000000000000000003000000000001000000 + cfg := msg[566:] + cfg[0] = 0 // 0 - simple proto, 1 - complex proto with "0Cxx" commands + cfg[4] = 4 + copy(cfg[8:], "\xfb\x07\x1f\x00") + cfg[22] = 3 + cfg[28] = 1 + return msg +} + +func (c *Conn) msg(size uint16) []byte { + b := make([]byte, size) + copy(b, "\x04\x02\x19\x0a") + binary.LittleEndian.PutUint16(b[4:], size-16) + copy(b[8:], "\x07\x04\x21\x00") + copy(b[12:], c.sid) + return b +} + +const ( + msgPing = iota + 1 + msgClientStart00 + msgClientStart20 + msgCommand + msgCounters + msgMediaChunk + msgMediaFrame + msgMediaLost + msgCh5 + msgUnknown0010 + msgUnknown0a08 + msgDafang0012 + msgDafang0071 +) + +// handleMsg will return parsed msg type or zero +func (c *Conn) handleMsg(msg []byte) int8 { + //log.Printf("<- %x", msg) + // off sample + // 0 0402 tutk magic + // 2 120a tutk version (120a, 190a...) + // 4 0800 msg size = len(b)-16 + // 6 0000 channel seq + // 8 28041200 msg type + // 14 0100 channel (not all msg) + // 28 0700 msg data (not all msg) + switch msg[8] { + case 0x28: + _ = c.Write(msgAckPing(msg)) + return msgPing + case 0x08: + switch ch := msg[14]; ch { + case 0: + return c.handleCh0(msg[28:]) + case 1: + return c.handleCh1(msg[28:]) + case 5: + return c.handleCh5(msg) + } + } + return 0 +} + +func (c *Conn) handleCh1(cmd []byte) int8 { + switch cid := string(cmd[:2]); cid { + case "\x00\x00": + _ = c.WriteCh1(c.msgAck0000(cmd)) + return msgClientStart00 + case "\x00\x20": + //_ = c.WriteCh1(c.msgAck0020(cmd)) + return msgClientStart20 + case "\x09\x00": // skip + return msgCounters + case "\x0a\x08": + _ = c.WriteCh1(c.msgAck0A08(cmd)) + return msgUnknown0a08 + } + return 0 +} + +func (c *Conn) handleCh5(msg []byte) int8 { + if len(msg) != 48 { + return 0 + } + + // <- [48] 0402190a 20000400 07042100 7ecc05000c0000007ecc93c456c2561f 5a97c2f101050000000000000000000000010000 + // -> [48] 0402190a 20000400 08041200 7ecc05000c0000007ecc93c456c2561f 5a97c2f141050000000000000000000000010000 + copy(msg[8:], "\x07\x04\x21\x00") + msg[32] = 0x41 + _ = c.Write(msg) + return msgCh5 +} + +const msgHhrSize = 28 +const cmdHdrSize = 24 + +func (c *Conn) msgAck0000(msg28 []byte) []byte { + const cmdDataSize = 36 + + msg := c.msg(msgHhrSize + cmdHdrSize + cmdDataSize) + + cmd := msg[msgHhrSize:] + copy(cmd, "\x00\x14\x0b\x00") + cmd[16] = cmdDataSize + copy(cmd[20:], msg28[20:24]) // request id (random) + + // It's better not to answer anything, so camera won't send anything to this channel. + //data := cmd[cmdHdrSize:] + //copy(data, msg28[len(msg28)-32:]) + return msg +} + +//func (c *Conn) msgAck0020(msg28 []byte) []byte { +// const cmdDataSize = 36 +// +// msg := c.msg(msgHhrSize + cmdHdrSize + cmdDataSize) +// +// cmd := msg[msgHhrSize:] +// copy(cmd, "\x00\x14\x0b\x00") +// cmd[16] = cmdDataSize +// copy(cmd[20:], msg28[20:24]) // request id (random) +// +// data := cmd[cmdHdrSize:] +// data[5] = 1 +// data[7] = 1 +// data[8] = 1 +// data[12] = 4 +// copy(data[16:], "\xfb\x07\x1f\x00") +// data[30] = 3 +// data[32] = 1 +// return msg +//} + +func (c *Conn) msgAck0A08(msg28 []byte) []byte { + msg := c.msg(48) + cmd := msg[msgHhrSize:] + copy(cmd, "\x0b\x00\x0b\x00") + copy(cmd[8:], msg28[8:10]) + return msg +} + +func msgAckPing(req []byte) []byte { + // <- [24] 0402120a 08000000 28041200 000000005b0d4202070aa8c0 + // -> [24] 04021a0a 08000000 27042100 000000005b0d4202070aa8c0 + req[8] = 0x27 + req[10] = 0x21 + return req +} diff --git a/pkg/xiaomi/tutk/proto_new.go b/pkg/xiaomi/tutk/proto_new.go new file mode 100644 index 00000000..8f84b91b --- /dev/null +++ b/pkg/xiaomi/tutk/proto_new.go @@ -0,0 +1,191 @@ +package tutk + +import ( + "encoding/binary" + "fmt" + "time" +) + +func (c *Conn) newMsgCtrl(ctrlType uint16, ctrlData []byte) []byte { + size := msgHhrSize + 28 + 4 + uint16(len(ctrlData)) + msg := c.msg(size) + + // 0 0070 command + // 2 0b00 version + // 4 1000 seq + // 6 0076 ??? + cmd := msg[msgHhrSize:] + copy(cmd, "\x00\x70\x0b\x00") + binary.LittleEndian.PutUint16(cmd[4:], c.seqSendCmd1) + c.seqSendCmd1++ + + // 8 0070 command (second time) + // 10 0300 seq + // 12 0100 chunks count + // 14 0000 chunk seq (starts from 0) + // 16 5500 size + // 18 0000 random msg id (always 0) + // 20 03000000 seq (second time) + // 24 00000000 + // 28 01010000 ctrlType + cmd[9] = 0x70 + cmd[12] = 1 + binary.LittleEndian.PutUint16(cmd[16:], size-52) + + binary.LittleEndian.PutUint16(cmd[10:], c.seqSendCmd2) + binary.LittleEndian.PutUint16(cmd[20:], c.seqSendCmd2) + c.seqSendCmd2++ + + data := cmd[28:] + binary.LittleEndian.PutUint16(data, ctrlType) + copy(data[4:], ctrlData) + return msg +} + +func (c *Conn) newHandlerCh0() func(msg []byte) int8 { + var waitData []byte + var waitSeq uint16 + + return func(cmd []byte) int8 { + switch cmd[0] { + case 0x07, 0x05: + flag := cmd[1] + + var cmd2 []byte + if flag&0b1000 == 0 { + // off sample + // 0 0700 command + // 2 0b00 version + // 4 2700 seq + // 6 0000 ??? + // 8 0700 command (second time) + // 10 1400 seq + // 12 1300 chunks count per this frame + // 14 1100 chunk seq, starts from 0 (0x20 for last chunk) + // 16 0004 frame data size + // 18 0000 random msg id (always 0) + // 20 02000000 previous frame seq, starts from 0 + // 24 03000000 current frame seq, starts from 1 + cmd2 = cmd[8:] + } else { + // off sample + // 0 070d0b00 + // 4 30000000 + // 8 5c965500 ??? + // 12 ffff0000 ??? + // 16 0701 fixed command + // 18 190001002000a802000006000000070000000 + cmd2 = cmd[16:] + } + + seq := binary.LittleEndian.Uint16(cmd2[2:]) + + // Check if this is first chunk for frame. + // Handle protocol bug "0x20 chunk seq for last chunk" and sometimes + // "0x20 chunk seq for first chunk if only one chunk". + if binary.LittleEndian.Uint16(cmd2[6:]) == 0 || binary.LittleEndian.Uint16(cmd2[4:]) == 1 { + waitData = waitData[:0] + waitSeq = seq + } else if seq != waitSeq { + return msgMediaLost + } + + if flag&0b0001 == 0 { + waitData = append(waitData, cmd2[20:]...) + waitSeq++ + return msgMediaChunk + } + + c.seqRecvPkt1 = seq + _ = c.WriteCh0(c.msgAckCounters()) + + data := cmd2[20:] + n := len(data) - 32 + waitData = append(waitData, data[:n]...) + + packetData := make([]byte, 32+len(waitData)) + copy(packetData, data[n:]) + copy(packetData[32:], waitData) + + select { + case c.rawPkt <- packetData: + default: + c.err = fmt.Errorf("%s: media queue is full", "tutk") + return -1 + } + return msgMediaFrame + + case 0x00: + _ = c.WriteCh0(c.msgAckCounters()) + c.seqRecvCmd2 = binary.LittleEndian.Uint16(cmd[2:]) + + switch cmd[1] { + case 0x10: + return msgUnknown0010 // unknown + case 0x70: + select { + case c.rawCmd <- cmd[28:]: + default: + } + return msgCommand // cmd from camera + } + + case 0x09: + // off sample + // 0 09000b00 cmd1 + // 4 0d000000 seqCmd1 + // 12 0000 seqRecvCmd2 + seq := binary.LittleEndian.Uint16(cmd[12:]) + if c.seqSendCmd1 > seq { + if c.cmdAck != nil { + c.cmdAck() + } + } + return msgCounters + + case 0x0a: + // seq sample + // 0 0a080b00 + // 4 03000000 + // 8 e2043200 + // 12 01000000 + _ = c.WriteCh0(c.msgAck0A08(cmd)) + return msgUnknown0a08 + } + + return 0 + } +} + +func (c *Conn) msgAckCounters() []byte { + msg := c.msg(msgHhrSize + cmdHdrSize) + + // off sample + // 0 09000b00 cmd1 + // 4 2700 seqCmd1 + // 6 0000 + // 8 1300 seqRecvPkt0 + // 10 2600 seqRecvPkt1 + // 12 0400 seqRecvCmd2 + // 14 00000000 + // 18 1400 seqSendCnt + // 20 d91a random + // 22 0000 + cmd := msg[msgHhrSize:] + copy(cmd, "\x09\x00\x0b\x00") + + binary.LittleEndian.PutUint16(cmd[4:], c.seqSendCmd1) + c.seqSendCmd1++ + + // seqRecvPkt0 stores previous value of seqRecvPkt1 + // don't understand why this needs + binary.LittleEndian.PutUint16(cmd[8:], c.seqRecvPkt0) + c.seqRecvPkt0 = c.seqRecvPkt1 + binary.LittleEndian.PutUint16(cmd[10:], c.seqRecvPkt1) + binary.LittleEndian.PutUint16(cmd[12:], c.seqRecvCmd2) + + binary.LittleEndian.PutUint16(cmd[18:], c.seqSendCnt) + c.seqSendCnt++ + binary.LittleEndian.PutUint16(cmd[20:], uint16(time.Now().UnixNano())) + return msg +} diff --git a/pkg/xiaomi/tutk/proto_old.go b/pkg/xiaomi/tutk/proto_old.go new file mode 100644 index 00000000..7ac4f803 --- /dev/null +++ b/pkg/xiaomi/tutk/proto_old.go @@ -0,0 +1,153 @@ +package tutk + +import ( + "encoding/binary" + "fmt" +) + +func (c *Conn) oldMsgCtrl(ctrlType uint16, ctrlData []byte) []byte { + size := msgHhrSize + cmdHdrSize + 4 + uint16(len(ctrlData)) + msg := c.msg(size) + + cmd := msg[msgHhrSize:] + copy(cmd, "\x00\x70\x0b\x00") + + binary.LittleEndian.PutUint16(cmd[4:], c.seqSendCmd1) + c.seqSendCmd1++ + + binary.LittleEndian.PutUint16(cmd[16:], size-52) + //binary.LittleEndian.PutUint32(cmd[20:], uint32(time.Now().UnixMilli())) + + data := cmd[cmdHdrSize:] + binary.LittleEndian.PutUint16(data, ctrlType) + copy(data[4:], ctrlData) + return msg +} + +func (c *Conn) oldHandlerCh0() func([]byte) int8 { + var waitSeq uint16 + var waitSize uint32 + var waitData []byte + + return func(cmd []byte) int8 { + // 0 01030800 command + version + // 4 00000000 fixed + // 8 ac880100 total size + // 12 6200 chunk seq + // 14 2000 ??? + // 16 cc00 size + // 18 0000 + // 20 01000000 fixed + + switch cmd[0] { + case 0x01: + switch cmd[1] { + case 0x03: + seq := binary.LittleEndian.Uint16(cmd[12:]) + if seq != waitSeq { + waitSeq = 0 + return msgMediaLost + } + if seq == 0 { + waitData = waitData[:0] + waitSize = binary.LittleEndian.Uint32(cmd[8:]) + 32 + } + + waitData = append(waitData, cmd[24:]...) + if n := uint32(len(waitData)); n < waitSize { + waitSeq++ + return msgMediaChunk + } else if n > waitSize { + waitSeq = 0 + return msgMediaLost + } + + // create a buffer for the header and collected data + packetData := make([]byte, waitSize) + // there's a header at the end - let's move it to the beginning + copy(packetData, waitData[waitSize-32:]) + copy(packetData[32:], waitData) + + select { + case c.rawPkt <- packetData: + default: + c.err = fmt.Errorf("%s: media queue is full", "tutk") + return -1 + } + + waitSeq = 0 + return msgMediaFrame + + case 0x04: + waitSize2 := binary.LittleEndian.Uint32(cmd[8:]) + waitData2 := cmd[24:] + + if uint32(len(waitData2)) != waitSize2 { + return -1 // shouldn't happened for audio + } + + packetData := make([]byte, waitSize2) + copy(packetData, waitData2) + + select { + case c.rawPkt <- packetData: + default: + c.err = fmt.Errorf("%s: media queue is full", "tutk") + return -1 + } + return msgMediaFrame + } + + case 0x00: + switch cmd[1] { + case 0x70: + _ = c.WriteCh0(c.msgAck0070(cmd)) + select { + case c.rawCmd <- cmd[24:]: + default: + } + return msgCommand + case 0x12: + _ = c.WriteCh0(c.msgAck0012(cmd)) + return msgDafang0012 + case 0x71: + if c.cmdAck != nil { + c.cmdAck() + } + return msgDafang0071 + } + } + + return 0 + } +} + +func (c *Conn) msgAck0070(msg28 []byte) []byte { + // <- [104] 0402120a 58000300 08041200 e6e8 0000 0c000000e6e839da66b0dc14 00700800010000000000000000000000 3400 00007625a02f ... + // -> [ 52] 0402190a 24000400 07042100 e6e8 0000 0c000000e6e839da66b0dc14 00710800010000000000000000000000 0000 00007625a02f + msg := c.msg(52) + + cmd := msg[msgHhrSize:] + copy(cmd, "\x00\x71\x0b\x00") + binary.LittleEndian.PutUint16(cmd[4:], c.seqSendCmd1) + c.seqSendCmd1++ + copy(cmd[8:], msg28[8:10]) + + return msg +} + +func (c *Conn) msgAck0012(msg28 []byte) []byte { + // <- [64] 0402120a 30000000 08041200 e6e800000c000000e6e839da66b0dc14 001208000000000000000000000000000c00000000000000 020000000100000001000000 + // -> [72] 0402190a 38000300 07042100 e6e800000c000000e6e839da66b0dc14 00130b000000000000000000000000001400000000000000 0200000001000000010000000000000000000000 + const size = 72 + msg := c.msg(size) + + cmd := msg[msgHhrSize:] + copy(cmd, "\x00\x13\x0b\x00") + cmd[16] = size - 52 // data size + + data := cmd[cmdHdrSize:] + copy(data, msg28[cmdHdrSize:]) + + return msg +} From d4dc670cb5fdab3759dc2c715b0ab1730d4288e4 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 2 Jan 2026 10:19:58 +0300 Subject: [PATCH 3/6] Add support two-way audio for Dafang camera --- pkg/xiaomi/backchannel.go | 5 +- pkg/xiaomi/producer.go | 12 ++-- pkg/xiaomi/tutk/conn.go | 3 +- pkg/xiaomi/tutk/proto.go | 75 +++++++++++++++++++++---- pkg/xiaomi/tutk/proto_new.go | 2 +- pkg/xiaomi/tutk/proto_old.go | 103 ++++++++++++++++++++++++----------- 6 files changed, 147 insertions(+), 53 deletions(-) diff --git a/pkg/xiaomi/backchannel.go b/pkg/xiaomi/backchannel.go index a6f57b81..17242b8e 100644 --- a/pkg/xiaomi/backchannel.go +++ b/pkg/xiaomi/backchannel.go @@ -23,7 +23,8 @@ func (p *Producer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv case core.CodecPCMA: var buf []byte - if p.model == "isa.camera.hlc6" { + switch p.model { + case "isa.camera.hlc6", "isa.camera.df3": dst := &core.Codec{Name: core.CodecPCML, ClockRate: 8000} transcode := pcm.Transcode(dst, track.Codec) @@ -36,7 +37,7 @@ func (p *Producer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv buf = buf[size:] } } - } else { + default: sender.Handler = func(pkt *rtp.Packet) { buf = append(buf, pkt.Payload...) const size = 8000 * 0.040 // 8bit 40 ms diff --git a/pkg/xiaomi/producer.go b/pkg/xiaomi/producer.go index f2ce4eda..805a3f86 100644 --- a/pkg/xiaomi/producer.go +++ b/pkg/xiaomi/producer.go @@ -140,13 +140,11 @@ func probe(client *miss.Client, channel, quality, audio uint8) ([]*core.Media, e Codecs: []*core.Codec{acodec}, }) - if client.Protocol() == "cs2+udp" { - medias = append(medias, &core.Media{ - Kind: core.KindAudio, - Direction: core.DirectionSendonly, - Codecs: []*core.Codec{acodec.Clone()}, - }) - } + medias = append(medias, &core.Media{ + Kind: core.KindAudio, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{acodec.Clone()}, + }) } return medias, nil diff --git a/pkg/xiaomi/tutk/conn.go b/pkg/xiaomi/tutk/conn.go index 37489ffa..1b0ea56e 100644 --- a/pkg/xiaomi/tutk/conn.go +++ b/pkg/xiaomi/tutk/conn.go @@ -65,6 +65,7 @@ type Conn struct { seqSendCmd1 uint16 seqSendCmd2 uint16 seqSendCnt uint16 + seqSendAud uint16 seqRecvPkt0 uint16 seqRecvPkt1 uint16 @@ -249,7 +250,7 @@ func (c *Conn) ReadPacket() ([]byte, error) { } func (c *Conn) WritePacket(data []byte) error { - panic("not implemented") + return c.WriteCh1(c.oldMsgAud(data)) } func genSID() []byte { diff --git a/pkg/xiaomi/tutk/proto.go b/pkg/xiaomi/tutk/proto.go index 86cf95af..a440900a 100644 --- a/pkg/xiaomi/tutk/proto.go +++ b/pkg/xiaomi/tutk/proto.go @@ -75,7 +75,10 @@ const ( msgMediaFrame msgMediaLost msgCh5 + msgUnknown0007 + msgUnknown0008 msgUnknown0010 + msgUnknown0013 msgUnknown0a08 msgDafang0012 msgDafang0071 @@ -110,16 +113,29 @@ func (c *Conn) handleMsg(msg []byte) int8 { } func (c *Conn) handleCh1(cmd []byte) int8 { + // Channel 1 used for two-way audio. It's important: + // - answer on 0000 command with exact config response (can't set simple proto) + // - send 0012 command at start + // - respond on every 0008 command for smooth playback switch cid := string(cmd[:2]); cid { - case "\x00\x00": + case "\x00\x00": // client start _ = c.WriteCh1(c.msgAck0000(cmd)) + _ = c.WriteCh1(c.msg0012()) return msgClientStart00 - case "\x00\x20": + case "\x00\x07": // time sync without data + _ = c.WriteCh1(c.msgAck0007(cmd)) + return msgUnknown0007 + case "\x00\x08": // time sync with data + _ = c.WriteCh1(c.msgAck0008(cmd)) + return msgUnknown0008 + case "\x00\x13": // ack for 0012 + return msgUnknown0013 + case "\x00\x20": // client start2 //_ = c.WriteCh1(c.msgAck0020(cmd)) return msgClientStart20 - case "\x09\x00": // skip + case "\x09\x00": // counters sync return msgCounters - case "\x0a\x08": + case "\x0a\x08": // unknown _ = c.WriteCh1(c.msgAck0A08(cmd)) return msgUnknown0a08 } @@ -143,8 +159,9 @@ const msgHhrSize = 28 const cmdHdrSize = 24 func (c *Conn) msgAck0000(msg28 []byte) []byte { - const cmdDataSize = 36 - + // <- 000008000000000000000000000000001a0200004f47c714 ... 00000000000000000100000004000000fb071f00000000000000000000000300 + // -> 00140b00000000000000000000000000200000004f47c714 00000000000000000100000004000000fb071f00000000000000000000000300 + const cmdDataSize = 32 msg := c.msg(msgHhrSize + cmdHdrSize + cmdDataSize) cmd := msg[msgHhrSize:] @@ -152,9 +169,9 @@ func (c *Conn) msgAck0000(msg28 []byte) []byte { cmd[16] = cmdDataSize copy(cmd[20:], msg28[20:24]) // request id (random) - // It's better not to answer anything, so camera won't send anything to this channel. - //data := cmd[cmdHdrSize:] - //copy(data, msg28[len(msg28)-32:]) + // Important to answer with same data. + data := cmd[cmdHdrSize:] + copy(data, msg28[len(msg28)-32:]) return msg } @@ -179,8 +196,46 @@ func (c *Conn) msgAck0000(msg28 []byte) []byte { // return msg //} +func (c *Conn) msg0012() []byte { + // -> 00120b000000000000000000000000000c00000000000000020000000100000001000000 + const dataSize = 12 + msg := c.msg(msgHhrSize + cmdHdrSize + dataSize) + cmd := msg[msgHhrSize:] + + copy(cmd, "\x00\x12\x0b\x00") + cmd[16] = dataSize + data := cmd[cmdHdrSize:] + + data[0] = 2 + data[4] = 1 + data[9] = 1 + return msg +} + +func (c *Conn) msgAck0007(msg28 []byte) []byte { + // <- 000708000000000000000000000000000c00000001000000000000001c551f7a00000000 + // -> 010a0b00000000000000000000000000000000000100000000000000 + msg := c.msg(msgHhrSize + 28) + cmd := msg[msgHhrSize:] + copy(cmd, "\x01\x0a\x0b\x00") + cmd[20] = 1 + return msg +} + +func (c *Conn) msgAck0008(msg28 []byte) []byte { + // <- 000808000000000000000000000000000000f9f0010000000200000050f31f7a + // -> 01090b0000000000000000000000000000000000010000000200000050f31f7a + msg := c.msg(msgHhrSize + 28) + cmd := msg[msgHhrSize:] + copy(cmd, "\x01\x09\x0b\x00") + copy(cmd[20:], msg28[20:]) + return msg +} + func (c *Conn) msgAck0A08(msg28 []byte) []byte { - msg := c.msg(48) + // <- 0a080b005b0000000b51590002000000 + // -> 0b000b00000001000b5103000300000000000000 + msg := c.msg(msgHhrSize + 20) cmd := msg[msgHhrSize:] copy(cmd, "\x0b\x00\x0b\x00") copy(cmd[8:], msg28[8:10]) diff --git a/pkg/xiaomi/tutk/proto_new.go b/pkg/xiaomi/tutk/proto_new.go index 8f84b91b..e7a08080 100644 --- a/pkg/xiaomi/tutk/proto_new.go +++ b/pkg/xiaomi/tutk/proto_new.go @@ -186,6 +186,6 @@ func (c *Conn) msgAckCounters() []byte { binary.LittleEndian.PutUint16(cmd[18:], c.seqSendCnt) c.seqSendCnt++ - binary.LittleEndian.PutUint16(cmd[20:], uint16(time.Now().UnixNano())) + binary.LittleEndian.PutUint16(cmd[20:], uint16(time.Now().UnixMilli())) return msg } diff --git a/pkg/xiaomi/tutk/proto_old.go b/pkg/xiaomi/tutk/proto_old.go index 7ac4f803..875c67a9 100644 --- a/pkg/xiaomi/tutk/proto_old.go +++ b/pkg/xiaomi/tutk/proto_old.go @@ -3,11 +3,12 @@ package tutk import ( "encoding/binary" "fmt" + "time" ) func (c *Conn) oldMsgCtrl(ctrlType uint16, ctrlData []byte) []byte { - size := msgHhrSize + cmdHdrSize + 4 + uint16(len(ctrlData)) - msg := c.msg(size) + dataSize := 4 + uint16(len(ctrlData)) + msg := c.msg(msgHhrSize + cmdHdrSize + dataSize) cmd := msg[msgHhrSize:] copy(cmd, "\x00\x70\x0b\x00") @@ -15,7 +16,7 @@ func (c *Conn) oldMsgCtrl(ctrlType uint16, ctrlData []byte) []byte { binary.LittleEndian.PutUint16(cmd[4:], c.seqSendCmd1) c.seqSendCmd1++ - binary.LittleEndian.PutUint16(cmd[16:], size-52) + binary.LittleEndian.PutUint16(cmd[16:], dataSize) //binary.LittleEndian.PutUint32(cmd[20:], uint32(time.Now().UnixMilli())) data := cmd[cmdHdrSize:] @@ -24,6 +25,43 @@ func (c *Conn) oldMsgCtrl(ctrlType uint16, ctrlData []byte) []byte { return msg } +const pktHdrSize = 32 + +func (c *Conn) oldMsgAud(pkt []byte) []byte { + // -> 01030b001d0000008802000000002800b0020bf501000000 ... 4f4455412000000088020000030400001d000000000000000bf51f7a9b0100000000000000000000 + hdr := pkt[:pktHdrSize] + payload := pkt[pktHdrSize:] + + n := uint16(len(payload)) + dataSize := n + 8 + 32 + msg := c.msg(msgHhrSize + cmdHdrSize + dataSize) + + // 0 01030b00 command + version + // 4 1d000000 seq + // 8 8802 media size (648) + // 10 00000000 + // 14 2800 tail (pkt header) size? + // 16 b002 size (648 + 8 + 32) + // 18 0bf5 random msg id (unixms) + // 20 01000000 fixed + cmd := msg[msgHhrSize:] + copy(cmd, "\x01\x03\x0b\x00") + binary.LittleEndian.PutUint16(cmd[4:], c.seqSendAud) + c.seqSendAud++ + binary.LittleEndian.PutUint16(cmd[8:], n) + cmd[14] = 0x28 // important! + binary.LittleEndian.PutUint16(cmd[16:], dataSize) + binary.LittleEndian.PutUint16(cmd[18:], uint16(time.Now().UnixMilli())) + cmd[20] = 1 + + data := cmd[cmdHdrSize:] + copy(data, payload) + copy(data[n:], "ODUA\x20\x00\x00\x00") + copy(data[n+8:], hdr) + + return msg +} + func (c *Conn) oldHandlerCh0() func([]byte) int8 { var waitSeq uint16 var waitSize uint32 @@ -34,13 +72,15 @@ func (c *Conn) oldHandlerCh0() func([]byte) int8 { // 4 00000000 fixed // 8 ac880100 total size // 12 6200 chunk seq - // 14 2000 ??? + // 14 2000 tail (pkt header) size? // 16 cc00 size // 18 0000 // 20 01000000 fixed switch cmd[0] { case 0x01: + var packetData []byte + switch cmd[1] { case 0x03: seq := binary.LittleEndian.Uint16(cmd[12:]) @@ -62,42 +102,41 @@ func (c *Conn) oldHandlerCh0() func([]byte) int8 { return msgMediaLost } + waitSeq = 0 + // create a buffer for the header and collected data - packetData := make([]byte, waitSize) + packetData = make([]byte, waitSize) // there's a header at the end - let's move it to the beginning copy(packetData, waitData[waitSize-32:]) copy(packetData[32:], waitData) - select { - case c.rawPkt <- packetData: - default: - c.err = fmt.Errorf("%s: media queue is full", "tutk") - return -1 - } - - waitSeq = 0 - return msgMediaFrame - case 0x04: + // This is audio from miss audio start command. MiHome not using miss commands. waitSize2 := binary.LittleEndian.Uint32(cmd[8:]) waitData2 := cmd[24:] if uint32(len(waitData2)) != waitSize2 { - return -1 // shouldn't happened for audio + return -1 // shouldn't happen for audio } - packetData := make([]byte, waitSize2) + packetData = make([]byte, waitSize2) copy(packetData, waitData2) - select { - case c.rawPkt <- packetData: - default: - c.err = fmt.Errorf("%s: media queue is full", "tutk") - return -1 - } - return msgMediaFrame + default: + return 0 } + // fix Dafang bug (timestamp in seconds) + binary.LittleEndian.PutUint64(packetData[16:], uint64(time.Now().UnixMilli())) + + select { + case c.rawPkt <- packetData: + default: + c.err = fmt.Errorf("%s: media queue is full", "tutk") + return -1 + } + return msgMediaFrame + case 0x00: switch cmd[1] { case 0x70: @@ -123,9 +162,9 @@ func (c *Conn) oldHandlerCh0() func([]byte) int8 { } func (c *Conn) msgAck0070(msg28 []byte) []byte { - // <- [104] 0402120a 58000300 08041200 e6e8 0000 0c000000e6e839da66b0dc14 00700800010000000000000000000000 3400 00007625a02f ... - // -> [ 52] 0402190a 24000400 07042100 e6e8 0000 0c000000e6e839da66b0dc14 00710800010000000000000000000000 0000 00007625a02f - msg := c.msg(52) + // <- 00700800010000000000000000000000340000007625a02f ... + // -> 00710800010000000000000000000000000000007625a02f + msg := c.msg(msgHhrSize + cmdHdrSize) cmd := msg[msgHhrSize:] copy(cmd, "\x00\x71\x0b\x00") @@ -137,14 +176,14 @@ func (c *Conn) msgAck0070(msg28 []byte) []byte { } func (c *Conn) msgAck0012(msg28 []byte) []byte { - // <- [64] 0402120a 30000000 08041200 e6e800000c000000e6e839da66b0dc14 001208000000000000000000000000000c00000000000000 020000000100000001000000 - // -> [72] 0402190a 38000300 07042100 e6e800000c000000e6e839da66b0dc14 00130b000000000000000000000000001400000000000000 0200000001000000010000000000000000000000 - const size = 72 - msg := c.msg(size) + // <- 001208000000000000000000000000000c00000000000000 020000000100000001000000 + // -> 00130b000000000000000000000000001400000000000000 0200000001000000010000000000000000000000 + const dataSize = 20 + msg := c.msg(msgHhrSize + cmdHdrSize + dataSize) cmd := msg[msgHhrSize:] copy(cmd, "\x00\x13\x0b\x00") - cmd[16] = size - 52 // data size + cmd[16] = dataSize data := cmd[cmdHdrSize:] copy(data, msg28[cmdHdrSize:]) From 6b4eb8ffb6e32d29bc81843b83e2f2ba036d6658 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 4 Jan 2026 00:09:32 +0300 Subject: [PATCH 4/6] Big rewrite tutk proto support --- examples/tutk_decoder/README.md | 5 + examples/tutk_decoder/main.go | 82 ++++++ internal/xiaomi/README.md | 26 +- internal/xiaomi/xiaomi.go | 107 ++++++-- pkg/h265/h265_test.go | 11 + pkg/tutk/conn.go | 264 +++++++++++++++++++ pkg/{xiaomi => }/tutk/crypto.go | 36 +++ pkg/tutk/crypto_test.go | 14 + pkg/tutk/helpers.go | 28 ++ pkg/tutk/session0.go | 163 ++++++++++++ pkg/tutk/session16.go | 378 +++++++++++++++++++++++++++ pkg/tutk/session25.go | 341 ++++++++++++++++++++++++ pkg/xiaomi/crypto/crypto.go | 68 +++++ pkg/xiaomi/legacy/client.go | 227 ++++++++++++++++ pkg/xiaomi/legacy/producer.go | 217 +++++++++++++++ pkg/xiaomi/{ => miss}/backchannel.go | 20 +- pkg/xiaomi/miss/client.go | 358 +++++++++++++------------ pkg/xiaomi/{ => miss}/cs2/conn.go | 27 +- pkg/xiaomi/miss/producer.go | 204 +++++++++++++++ pkg/xiaomi/producer.go | 224 +--------------- pkg/xiaomi/tutk/README.md | 63 ----- pkg/xiaomi/tutk/conn.go | 262 ------------------- pkg/xiaomi/tutk/proto.go | 251 ------------------ pkg/xiaomi/tutk/proto_new.go | 191 -------------- pkg/xiaomi/tutk/proto_old.go | 192 -------------- 25 files changed, 2376 insertions(+), 1383 deletions(-) create mode 100644 examples/tutk_decoder/README.md create mode 100644 examples/tutk_decoder/main.go create mode 100644 pkg/tutk/conn.go rename pkg/{xiaomi => }/tutk/crypto.go (78%) create mode 100644 pkg/tutk/crypto_test.go create mode 100644 pkg/tutk/helpers.go create mode 100644 pkg/tutk/session0.go create mode 100644 pkg/tutk/session16.go create mode 100644 pkg/tutk/session25.go create mode 100644 pkg/xiaomi/crypto/crypto.go create mode 100644 pkg/xiaomi/legacy/client.go create mode 100644 pkg/xiaomi/legacy/producer.go rename pkg/xiaomi/{ => miss}/backchannel.go (76%) rename pkg/xiaomi/{ => miss}/cs2/conn.go (93%) create mode 100644 pkg/xiaomi/miss/producer.go delete mode 100644 pkg/xiaomi/tutk/README.md delete mode 100644 pkg/xiaomi/tutk/conn.go delete mode 100644 pkg/xiaomi/tutk/proto.go delete mode 100644 pkg/xiaomi/tutk/proto_new.go delete mode 100644 pkg/xiaomi/tutk/proto_old.go diff --git a/examples/tutk_decoder/README.md b/examples/tutk_decoder/README.md new file mode 100644 index 00000000..197bd820 --- /dev/null +++ b/examples/tutk_decoder/README.md @@ -0,0 +1,5 @@ +# tutk_decoder + +1. Wireshark > Select any packet > Follow > UDP Stream +2. Wireshark > File > Export Packet Dissections > As JSON > Displayed, Values +3. `tutk_decoder wireshark.json decoded.txt` diff --git a/examples/tutk_decoder/main.go b/examples/tutk_decoder/main.go new file mode 100644 index 00000000..0b6d90a9 --- /dev/null +++ b/examples/tutk_decoder/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "log" + "os" + "strings" + + "github.com/AlexxIT/go2rtc/pkg/tutk" +) + +func main() { + if len(os.Args) != 3 { + fmt.Println("Usage: tutk_decoder wireshark.json decoded.txt") + return + } + + src, err := os.Open(os.Args[1]) + if err != nil { + log.Fatal(err) + } + defer src.Close() + + dst, err := os.Create(os.Args[2]) + if err != nil { + log.Fatal(err) + } + defer dst.Close() + + var items []item + if err = json.NewDecoder(src).Decode(&items); err != nil { + log.Fatal(err) + } + + var b []byte + + for _, v := range items { + if v.Source.Layers.Data.DataData == "" { + continue + } + + s := strings.ReplaceAll(v.Source.Layers.Data.DataData, ":", "") + b, err = hex.DecodeString(s) + if err != nil { + log.Fatal(err) + } + + tutk.ReverseTransCodePartial(b, b) + + ts := v.Source.Layers.Frame.FrameTimeRelative + + _, _ = fmt.Fprintf(dst, "%8s: %s -> %s [%4d] %x\n", + ts[:len(ts)-6], + v.Source.Layers.Ip.IpSrc, v.Source.Layers.Ip.IpDst, + len(b), b) + } +} + +type item struct { + Source struct { + Layers struct { + Frame struct { + FrameTimeRelative string `json:"frame.time_relative"` + FrameNumber string `json:"frame.number"` + } `json:"frame"` + Ip struct { + IpSrc string `json:"ip.src"` + IpDst string `json:"ip.dst"` + } `json:"ip"` + Udp struct { + UdpSrcport string `json:"udp.srcport"` + UdpDstport string `json:"udp.dstport"` + } `json:"udp"` + Data struct { + DataData string `json:"data.data"` + DataLen string `json:"data.len"` + } `json:"data"` + } `json:"layers"` + } `json:"_source"` +} diff --git a/internal/xiaomi/README.md b/internal/xiaomi/README.md index 80d98beb..f46dcdd1 100644 --- a/internal/xiaomi/README.md +++ b/internal/xiaomi/README.md @@ -1,11 +1,21 @@ # Xiaomi +**Added in v1.9.13. Improved in v1.9.14.** + This source allows you to view cameras from the [Xiaomi Mi Home](https://home.mi.com/) ecosystem. +Since 2020, Xiaomi has introduced a unified protocol for cameras called `miss`. I think it means **Mi Secure Streaming**. Until this point, the camera protocols were in chaos. Almost every model had different authorization, encryption, command lists, and media packet formats. + +Go2rtc support two formats: `xiaomi/mess` and `xiaomi/legacy`. +And multiple P2P protocols: `cs2+udp`, `cs2+tcp`, several versions of `tutk+udp`. + +Almost all cameras in the `xiaomi/mess` format and the `cs2` protocol work well. +Older `xiaomi/legacy` format cameras may have support issues. +The `tutk` protocol is the worst thing that's ever happened to the P2P world. It works terribly. + **Important:** -1. **Not all cameras are supported**. There are several P2P protocol vendors in the Xiaomi ecosystem. -Currently, the **CS2** vendor is supported. However, the **TUTK** vendor is not supported. +1. **Not all cameras are supported**. The list of supported cameras is collected in [this issue](https://github.com/AlexxIT/go2rtc/issues/1982). 2. Each time you connect to the camera, you need internet access to obtain encryption keys. 3. Connection to the camera is local only. @@ -21,7 +31,7 @@ Currently, the **CS2** vendor is supported. However, the **TUTK** vendor is not 1. Goto go2rtc WebUI > Add > Xiaomi > Login with username and password 2. Receive verification code by email or phone if required. 3. Complete the captcha if required. -4. If everything is OK, your account will be added and you can load cameras from it. +4. If everything is OK, your account will be added, and you can load cameras from it. **Example** @@ -35,16 +45,20 @@ streams: ## Configuration -You can change camera's quality: `subtype=hd/sd/auto` +Quality in the `miss` protocol is specified by a number from 0 to 5. Usually 0 means auto, 1 - sd, 2 - hd. +Go2rtc by default sets quality to 2. But some new cameras have HD quality at number 3. +Old cameras may have broken codec settings at number 3, so this number should not be set for all cameras. + +You can change camera's quality: `subtype=hd/sd/auto/0-5`. ```yaml streams: xiaomi1: xiaomi://***&subtype=sd ``` -You can use second channel for Dual cameras: `channel=1` +You can use second channel for Dual cameras: `channel=2`. ```yaml streams: - xiaomi1: xiaomi://***&channel=1 + xiaomi1: xiaomi://***&channel=2 ``` diff --git a/internal/xiaomi/xiaomi.go b/internal/xiaomi/xiaomi.go index f5f9b5bd..a27cb9f6 100644 --- a/internal/xiaomi/xiaomi.go +++ b/internal/xiaomi/xiaomi.go @@ -15,7 +15,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/xiaomi" - "github.com/AlexxIT/go2rtc/pkg/xiaomi/miss" + "github.com/AlexxIT/go2rtc/pkg/xiaomi/crypto" ) func Init() { @@ -65,28 +65,96 @@ func getCloud(userID string) (*xiaomi.Cloud, error) { return cloud, nil } +func cloudRequest(userID, region, apiURL, params string) ([]byte, error) { + cloud, err := getCloud(userID) + if err != nil { + return nil, err + } + return cloud.Request(GetBaseURL(region), apiURL, params, nil) +} + +func cloudUserRequest(user *url.Userinfo, apiURL, params string) ([]byte, error) { + userID := user.Username() + region, _ := user.Password() + return cloudRequest(userID, region, apiURL, params) +} + func getCameraURL(url *url.URL) (string, error) { - clientPublic, clientPrivate, err := miss.GenerateKey() + model := url.Query().Get("model") + + // It is not known which models need to be awakened. + // Probably all the doorbells and all the battery cameras. + if strings.Contains(model, ".cateye.") { + _ = wakeUpCamera(url) + } + + // The getMissURL request has a fallback to getP2PURL. + // But for known models we can save one request to the cloud. + if xiaomi.IsLegacy(model) { + return getP2PURL(url) + } + return getMissURL(url) +} + +func getP2PURL(url *url.URL) (string, error) { + query := url.Query() + + clientPublic, clientPrivate, err := crypto.GenerateKey() + if err != nil { + return "", err + } + + params := fmt.Sprintf(`{"did":"%s","toSignAppData":"%x"}`, query.Get("did"), clientPublic) + + userID := url.User.Username() + region, _ := url.User.Password() + res, err := cloudRequest(userID, region, "/device/devicepass", params) + if err != nil { + return "", err + } + + var v struct { + UID string `json:"p2p_id"` + Password string `json:"password"` + PublicKey string `json:"p2p_dev_public_key"` + Sign string `json:"signForAppData"` + } + if err = json.Unmarshal(res, &v); err != nil { + return "", err + } + + query.Set("uid", v.UID) + + if v.Sign != "" { + query.Set("client_public", hex.EncodeToString(clientPublic)) + query.Set("client_private", hex.EncodeToString(clientPrivate)) + query.Set("device_public", v.PublicKey) + query.Set("sign", v.Sign) + } else { + query.Set("password", v.Password) + } + + url.RawQuery = query.Encode() + return url.String(), nil +} + +func getMissURL(url *url.URL) (string, error) { + clientPublic, clientPrivate, err := crypto.GenerateKey() if err != nil { return "", err } query := url.Query() - params := fmt.Sprintf( - `{"app_pubkey":"%x","did":"%s","support_vendors":"CS2"}`, + `{"app_pubkey":"%x","did":"%s","support_vendors":"TUTK_CS2_MTP"}`, clientPublic, query.Get("did"), ) - cloud, err := getCloud(url.User.Username()) - if err != nil { - return "", err - } - - region, _ := url.User.Password() - - res, err := cloud.Request(GetBaseURL(region), "/v2/device/miss_get_vendor", params, nil) + res, err := cloudUserRequest(url.User, "/v2/device/miss_get_vendor", params) if err != nil { + if strings.Contains(err.Error(), "no available vendor support") { + return getP2PURL(url) + } return "", err } @@ -132,6 +200,13 @@ func getVendorName(i byte) string { return fmt.Sprintf("%d", i) } +func wakeUpCamera(url *url.URL) error { + const params = `{"id":1,"method":"wakeup","params":{"video":"1"}}` + did := url.Query().Get("did") + _, err := cloudUserRequest(url.User, "/home/rpc/"+did, params) + return err +} + func apiXiaomi(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": @@ -158,14 +233,8 @@ func apiDeviceList(w http.ResponseWriter, r *http.Request) { } err := func() error { - cloud, err := getCloud(user) - if err != nil { - return err - } - region := query.Get("region") - - res, err := cloud.Request(GetBaseURL(region), "/v2/home/device_list_page", "{}", nil) + res, err := cloudRequest(user, region, "/v2/home/device_list_page", "{}") if err != nil { return err } diff --git a/pkg/h265/h265_test.go b/pkg/h265/h265_test.go index 75fa03d7..278e09a3 100644 --- a/pkg/h265/h265_test.go +++ b/pkg/h265/h265_test.go @@ -17,3 +17,14 @@ func TestDecodeSPS(t *testing.T) { require.Equal(t, uint16(5120), sps.Width()) require.Equal(t, uint16(1440), sps.Height()) } + +func TestDecodeSPS2(t *testing.T) { + s := "QgEBIUAAAAMAkAAAAwAAAwCWoAUCAWlnpbkShc1AQIC4QAAAAwBAAAAFFEn/eEAOpgAV+V8IBBA=" + b, err := base64.StdEncoding.DecodeString(s) + require.Nil(t, err) + + sps := DecodeSPS(b) + require.NotNil(t, sps) + require.Equal(t, uint16(640), sps.Width()) + require.Equal(t, uint16(360), sps.Height()) +} diff --git a/pkg/tutk/conn.go b/pkg/tutk/conn.go new file mode 100644 index 00000000..e0610690 --- /dev/null +++ b/pkg/tutk/conn.go @@ -0,0 +1,264 @@ +package tutk + +import ( + "fmt" + "io" + "net" + "sync" + "sync/atomic" + "time" +) + +func Dial(host, uid, username, password string) (*Conn, error) { + addr, err := net.ResolveUDPAddr("udp", host) + if err != nil { + // Default port for listening incoming LAN connections. + // Important. It's not using for real connection. + addr = &net.UDPAddr{IP: net.ParseIP(host), Port: 32761} + } + + udpConn, err := net.ListenUDP("udp", nil) + if err != nil { + return nil, err + } + + c := &Conn{UDPConn: udpConn, addr: addr} + + sid := GenSessionID() + + _ = c.SetDeadline(time.Now().Add(5 * time.Second)) + + if addr.Port != 10001 { + err = c.connectDirect(uid, sid) + } else { + err = c.connectRemote(uid, sid) + } + if err != nil { + _ = c.Close() + return nil, err + } + + if c.ver[0] >= 25 { + c.session = NewSession25(c, sid) + } else { + c.session = NewSession16(c, sid) + } + + if err = c.clientStart(username, password); err != nil { + _ = c.Close() + return nil, err + } + + go c.worker() + + return c, nil +} + +type Conn struct { + *net.UDPConn + addr *net.UDPAddr + session Session + + ver []byte + err error + cmdMu sync.Mutex + cmdAck func() +} + +// Read overwrite net.Conn +func (c *Conn) Read(buf []byte) (n int, err error) { + for { + var addr *net.UDPAddr + if n, addr, err = c.UDPConn.ReadFromUDP(buf); err != nil { + return 0, err + } + + if string(c.addr.IP) != string(addr.IP) || n < 16 { + continue // skip messages from another IP + } + + if c.addr.Port != addr.Port { + c.addr.Port = addr.Port + } + + ReverseTransCodePartial(buf, buf[:n]) + //log.Printf("<- %x", buf[:n]) + return n, nil + } +} + +// Write overwrite net.Conn +func (c *Conn) Write(b []byte) (n int, err error) { + //log.Printf("-> %x", b) + return c.UDPConn.WriteToUDP(TransCodePartial(nil, b), c.addr) +} + +// RemoteAddr overwrite net.Conn +func (c *Conn) RemoteAddr() net.Addr { + return c.addr +} + +func (c *Conn) Protocol() string { + return "tutk+udp" +} + +func (c *Conn) Version() string { + if len(c.ver) == 1 { + return fmt.Sprintf("TUTK/%d", c.ver[0]) + } + return fmt.Sprintf("TUTK/%d SDK %d.%d.%d.%d", c.ver[0], c.ver[1], c.ver[2], c.ver[3], c.ver[4]) +} + +func (c *Conn) ReadCommand() (ctrlType uint32, ctrlData []byte, err error) { + return c.session.RecvIOCtrl() +} + +func (c *Conn) WriteCommand(ctrlType uint32, ctrlData []byte) error { + c.cmdMu.Lock() + defer c.cmdMu.Unlock() + + var repeat atomic.Int32 + repeat.Store(5) + + timeout := time.NewTicker(time.Second) + defer timeout.Stop() + + c.cmdAck = func() { + repeat.Store(0) + timeout.Reset(1) + } + + buf := c.session.SendIOCtrl(ctrlType, ctrlData) + + for { + if err := c.session.SessionWrite(0, buf); err != nil { + return err + } + <-timeout.C + r := repeat.Add(-1) + if r < 0 { + return nil + } + if r == 0 { + return fmt.Errorf("%s: can't send command %d", "tutk", ctrlType) + } + } +} + +func (c *Conn) ReadPacket() (hdr, payload []byte, err error) { + return c.session.RecvFrameData() +} + +func (c *Conn) WritePacket(hdr, payload []byte) error { + buf := c.session.SendFrameData(hdr, payload) + return c.session.SessionWrite(1, buf) +} + +func (c *Conn) Error() error { + if c.err != nil { + return c.err + } + return io.EOF +} + +func (c *Conn) worker() { + defer c.session.Close() + + buf := make([]byte, 1200) + + for { + n, err := c.Read(buf) + if err != nil { + c.err = fmt.Errorf("%s: %w", "tutk", err) + return + } + + switch c.handleMsg(buf[:n]) { + case msgUnknown: + fmt.Printf("tutk: unknown msg: %x\n", buf[:n]) + case msgError: + return + case msgCommandAck: + if c.cmdAck != nil { + c.cmdAck() + } + } + } +} + +const ( + msgUnknown = iota + msgError + msgPing + msgUnknownPing + msgClientStart + msgClientStart2 + msgClientStartAck2 + msgCommand + msgCommandAck + msgCounters + msgMediaChunk + msgMediaFrame + msgMediaReorder + msgMediaLost + msgCh5 + + msgUnknown0007 // time sync without data? + msgUnknown0008 // time sync with data? + msgUnknown0010 + msgUnknown0013 + msgUnknown0900 + msgUnknown0a08 + msgUnknownCh1c + msgDafang0012 +) + +func (c *Conn) handleMsg(msg []byte) int { + // off sample + // 0 0402 tutk magic + // 2 120a tutk version (120a, 190a...) + // 4 0800 msg size = len(b)-16 + // 6 0000 channel seq + // 8 28041200 msg type + // 14 0100 channel (not all msg) + // 28 0700 msg data (not all msg) + switch msg[8] { + case 0x08: + switch ch := msg[14]; ch { + case 0, 1: + return c.session.SessionRead(ch, msg[28:]) + case 5: + if len(msg) == 48 { + _, _ = c.Write(msgAckCh5(msg)) + return msgCh5 + } + case 0x1c: + return msgUnknownCh1c + } + case 0x18: + return msgUnknownPing + case 0x28: + if len(msg) == 24 { + _, _ = c.Write(msgAckPing(msg)) + return msgPing + } + } + return msgUnknown +} + +func msgAckPing(msg []byte) []byte { + // <- [24] 0402120a 08000000 28041200 000000005b0d4202070aa8c0 + // -> [24] 04021a0a 08000000 27042100 000000005b0d4202070aa8c0 + msg[8] = 0x27 + msg[10] = 0x21 + return msg +} + +func msgAckCh5(msg []byte) []byte { + // <- [48] 0402190a 20000400 07042100 7ecc05000c0000007ecc93c456c2561f 5a97c2f101050000000000000000000000010000 + // -> [48] 0402190a 20000400 08041200 7ecc05000c0000007ecc93c456c2561f 5a97c2f141050000000000000000000000010000 + msg[8] = 0x07 + msg[10] = 0x21 + msg[32] = 0x41 + return msg +} diff --git a/pkg/xiaomi/tutk/crypto.go b/pkg/tutk/crypto.go similarity index 78% rename from pkg/xiaomi/tutk/crypto.go rename to pkg/tutk/crypto.go index feeb31f7..6b306255 100644 --- a/pkg/xiaomi/tutk/crypto.go +++ b/pkg/tutk/crypto.go @@ -139,3 +139,39 @@ func swap(dst, src []byte, n int) { } copy(dst, src[:n]) } + +const delta = 0x9e3779b9 + +func XXTEADecrypt(dst, src, key []byte) { + const n = int8(4) // support only 16 bytes src + + var w, k [n]uint32 + for i := int8(0); i < n; i++ { + w[i] = binary.LittleEndian.Uint32(src) + k[i] = binary.LittleEndian.Uint32(key) + src = src[4:] + key = key[4:] + } + + rounds := 52/n + 6 + sum := uint32(rounds) * delta + for ; rounds > 0; rounds-- { + w0 := w[0] + i2 := int8((sum >> 2) & 3) + for i := n - 1; i >= 0; i-- { + wi := w[(i-1)&3] + ki := k[i^i2] + t1 := (w0 ^ sum) + (wi ^ ki) + t2 := (wi >> 5) ^ (w0 << 2) + t3 := (w0 >> 3) ^ (wi << 4) + w[i] -= t1 ^ (t2 + t3) + w0 = w[i] + } + sum -= delta + } + + for _, i := range w { + binary.LittleEndian.PutUint32(dst, i) + dst = dst[4:] + } +} diff --git a/pkg/tutk/crypto_test.go b/pkg/tutk/crypto_test.go new file mode 100644 index 00000000..1f1be3f2 --- /dev/null +++ b/pkg/tutk/crypto_test.go @@ -0,0 +1,14 @@ +package tutk + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestXXTEADecrypt(t *testing.T) { + buf := []byte("WERhJxb87WF3zgPa") + key := []byte("GAgDiwVPg2E4GMke") + XXTEADecrypt(buf, buf, key) + require.Equal(t, "\xc4\xa6\x2c\xa1\x10\x64\x17\xa5\xda\x02\xe1\x62\xa5\xf0\x62\x71", string(buf)) +} diff --git a/pkg/tutk/helpers.go b/pkg/tutk/helpers.go new file mode 100644 index 00000000..118119be --- /dev/null +++ b/pkg/tutk/helpers.go @@ -0,0 +1,28 @@ +package tutk + +import "encoding/binary" + +// https://github.com/seydx/tutk_wyze#11-codec-reference +const ( + CodecH264 = 0x4e + CodecH265 = 0x50 + CodecPCMA = 0x8a + CodecPCML = 0x8c + CodecAAC = 0x88 +) + +func ICAM(cmd uint32, args ...byte) []byte { + // 0 4943414d ICAM + // 4 d807ff00 command + // 8 00000000000000 + // 15 02 args count + // 16 00000000000000 + // 23 0101 args + n := byte(len(args)) + b := make([]byte, 23+n) + copy(b, "ICAM") + binary.LittleEndian.PutUint32(b[4:], cmd) + b[15] = n + copy(b[23:], args) + return b +} diff --git a/pkg/tutk/session0.go b/pkg/tutk/session0.go new file mode 100644 index 00000000..1f1bbc7e --- /dev/null +++ b/pkg/tutk/session0.go @@ -0,0 +1,163 @@ +package tutk + +import ( + "bytes" + "encoding/binary" + "net" + "time" +) + +func (c *Conn) connectDirect(uid string, sid []byte) error { + res, err := writeAndWait( + c, func(res []byte) bool { return bytes.Index(res, []byte("\x02\x06\x12\x00")) == 8 }, + ConnectByUID(stageBroadcast, uid, sid), + ) + if err != nil { + return err + } + + n := len(res) // should be 200 + c.ver = []byte{res[2], res[n-13], res[n-14], res[n-15], res[n-16]} + + _, err = c.Write(ConnectByUID(stageDirect, uid, sid)) + return err +} + +func (c *Conn) connectRemote(uid string, sid []byte) error { + res, err := writeAndWait( + c, func(res []byte) bool { return bytes.Index(res, []byte("\x01\x03\x43")) == 8 }, + ConnectByUID(stageGetRemoteIP, uid, sid), + ) + if err != nil { + return err + } + + // Read real IP from cloud server response. + // Important ot use net.IPv4 because slice will be 16 bytes. + c.addr.IP = net.IPv4(res[40], res[41], res[42], res[43]) + c.addr.Port = int(binary.BigEndian.Uint16(res[38:])) + + res, err = writeAndWait( + c, func(res []byte) bool { return bytes.Index(res, []byte("\x04\x04\x33")) == 8 }, + ConnectByUID(stageRemoteAck, uid, sid), + ) + if err != nil { + return err + } + + if len(res) == 52 { + c.ver = []byte{res[2], res[51], res[50], res[49], res[48]} + } else { + c.ver = []byte{res[2]} + } + + _, err = c.Write(ConnectByUID(stageRemoteOK, uid, sid)) + return err +} + +func (c *Conn) clientStart(username, password string) error { + _, err := writeAndWait( + c, func(res []byte) bool { + return len(res) >= 84 && res[28] == 0 && (res[29] == 0x14 || res[29] == 0x21) + }, + c.session.ClientStart(0, username, password), + c.session.ClientStart(1, username, password), + ) + return err +} + +func writeAndWait(conn net.Conn, ok func(res []byte) bool, req ...[]byte) ([]byte, error) { + var t *time.Timer + t = time.AfterFunc(1, func() { + for _, b := range req { + if _, err := conn.Write(b); err != nil { + return + } + } + if t != nil { + t.Reset(time.Second) + } + }) + defer t.Stop() + + buf := make([]byte, 1200) + + for { + n, err := conn.Read(buf) + if err != nil { + return nil, err + } + + if ok(buf[:n]) { + return buf[:n], nil + } + } +} + +const ( + magic = "\x04\x02\x19" // include version 0x19 + sdkVersion = "\x06\x00\x03\x03" // 3.3.0.6 +) + +const ( + stageBroadcast = iota + 1 + stageDirect + stageGetPublicIP + stageGetRemoteIP + stageRemoteReq + stageRemoteAck + stageRemoteOK +) + +func ConnectByUID(stage byte, uid string, sid8 []byte) []byte { + var b []byte + + switch stage { + case stageBroadcast, stageDirect: + b = make([]byte, 68) + copy(b[8:], "\x01\x06\x21") + copy(b[52:], sdkVersion) + copy(b[56:], sid8) + b[64] = stage // 1 or 2 + + case stageGetPublicIP: + b = make([]byte, 54) + copy(b[8:], "\x07\x10\x18") + + case stageGetRemoteIP: + b = make([]byte, 112) + copy(b[8:], "\x03\x02\x34") + copy(b[100:], sid8) + b[108] = stageDirect + + case stageRemoteReq: + b = make([]byte, 52) + copy(b[8:], "\x01\x04\x33") + copy(b[36:], sid8) + copy(b[48:], sdkVersion) + + case stageRemoteAck: + b = make([]byte, 44) + copy(b[8:], "\x02\x04\x33") + copy(b[36:], sid8) + + case stageRemoteOK: + b = make([]byte, 52) + copy(b[8:], "\x04\x04\x33") + copy(b[36:], sid8) + copy(b[48:], sdkVersion) + } + + copy(b, magic) + b[3] = 0x02 // connection stage + binary.LittleEndian.PutUint16(b[4:], uint16(len(b))-16) + copy(b[16:], uid) + + return b +} + +func GenSessionID() []byte { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, uint64(time.Now().UnixNano())) + return b +} diff --git a/pkg/tutk/session16.go b/pkg/tutk/session16.go new file mode 100644 index 00000000..47110dd3 --- /dev/null +++ b/pkg/tutk/session16.go @@ -0,0 +1,378 @@ +package tutk + +import ( + "bytes" + "encoding/binary" + "io" + "net" + "time" +) + +type Session interface { + Close() error + + ClientStart(i byte, username, password string) []byte + + SendIOCtrl(ctrlType uint32, ctrlData []byte) []byte + SendFrameData(frameInfo, frameData []byte) []byte + + RecvIOCtrl() (ctrlType uint32, ctrlData []byte, err error) + RecvFrameData() (frameInfo, frameData []byte, err error) + + SessionRead(chID byte, buf []byte) int + SessionWrite(chID byte, buf []byte) error +} + +func NewSession16(conn net.Conn, sid8 []byte) *Session16 { + sid16 := make([]byte, 16) + copy(sid16[8:], sid8) + copy(sid16, sid8[:2]) + sid16[4] = 0x0c + + return &Session16{ + conn: conn, + sid16: sid16, + rawCmd: make(chan []byte, 10), + rawPkt: make(chan [2][]byte, 100), + } +} + +type Session16 struct { + conn net.Conn + sid16 []byte + + rawCmd chan []byte + rawPkt chan [2][]byte + + seqSendCh0 uint16 + seqSendCh1 uint16 + + seqSendCmd1 uint16 + seqSendAud uint16 + + waitSeq uint16 + waitSize int + waitData []byte +} + +func (s *Session16) Close() error { + close(s.rawCmd) + close(s.rawPkt) + return nil +} + +func (s *Session16) Msg(size uint16) []byte { + b := make([]byte, size) + copy(b, magic) + b[3] = 0x0a // connected stage + binary.LittleEndian.PutUint16(b[4:], size-16) + copy(b[8:], "\x07\x04\x21") // client request + copy(b[12:], s.sid16) + return b +} + +const ( + msgHhrSize = 28 + cmdHdrSize = 24 +) + +func (s *Session16) ClientStart(i byte, username, password string) []byte { + const size = 566 + 32 + msg := s.Msg(size) + + // 0 00000b0000000000000000000000000022020000fcfc7284 + // 24 4d69737300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + // 281 636c69656e740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + // 538 0100000004000000fb071f000000000000000000000003000000000001000000 + cmd := msg[msgHhrSize:] + copy(cmd, "\x00\x00\x0b\x00") + binary.LittleEndian.PutUint16(cmd[16:], size-52) + if i == 0 { + cmd[18] = 1 + } else { + cmd[1] = 0x20 + } + binary.LittleEndian.PutUint32(cmd[20:], uint32(time.Now().UnixMilli())) + + // important values for some cameras (not for df3) + data := cmd[cmdHdrSize:] + copy(data, username) + copy(data[257:], password) + + // 0100000004000000fb071f000000000000000000000003000000000001000000 + cfg := data[257+257:] + //cfg[0] = 1 // 0 - simple proto, 1 - complex proto with "0Cxx" commands + cfg[4] = 4 + copy(cfg[8:], "\xfb\x07\x1f\x00") + cfg[22] = 3 + //cfg[28] = 1 // unknown + return msg +} + +func (s *Session16) SendIOCtrl(ctrlType uint32, ctrlData []byte) []byte { + dataSize := 4 + uint16(len(ctrlData)) + msg := s.Msg(msgHhrSize + cmdHdrSize + dataSize) + + cmd := msg[msgHhrSize:] + copy(cmd, "\x00\x70\x0b\x00") + + s.seqSendCmd1++ // start from 1, important! + binary.LittleEndian.PutUint16(cmd[4:], s.seqSendCmd1) + + binary.LittleEndian.PutUint16(cmd[16:], dataSize) + binary.LittleEndian.PutUint32(cmd[20:], uint32(time.Now().UnixMilli())) + + data := cmd[cmdHdrSize:] + binary.LittleEndian.PutUint32(data, ctrlType) + copy(data[4:], ctrlData) + return msg +} + +func (s *Session16) SendFrameData(frameInfo, frameData []byte) []byte { + // -> 01030b001d0000008802000000002800b0020bf501000000 ... 4f4455412000000088020000030400001d000000000000000bf51f7a9b0100000000000000000000 + + n := uint16(len(frameData)) + dataSize := n + 8 + 32 + msg := s.Msg(msgHhrSize + cmdHdrSize + dataSize) + + // 0 01030b00 command + version + // 4 1d000000 seq + // 8 8802 media size (648) + // 10 00000000 + // 14 2800 tail (pkt header) size? + // 16 b002 size (648 + 8 + 32) + // 18 0bf5 random msg id (unixms) + // 20 01000000 fixed + cmd := msg[msgHhrSize:] + copy(cmd, "\x01\x03\x0b\x00") + binary.LittleEndian.PutUint16(cmd[4:], s.seqSendAud) + s.seqSendAud++ + binary.LittleEndian.PutUint16(cmd[8:], n) + cmd[14] = 0x28 // important! + binary.LittleEndian.PutUint16(cmd[16:], dataSize) + binary.LittleEndian.PutUint16(cmd[18:], uint16(time.Now().UnixMilli())) + cmd[20] = 1 + + data := cmd[cmdHdrSize:] + copy(data, frameData) + copy(data[n:], "ODUA\x20\x00\x00\x00") + copy(data[n+8:], frameInfo) + + return msg +} + +func (s *Session16) RecvIOCtrl() (ctrlType uint32, ctrlData []byte, err error) { + buf, ok := <-s.rawCmd + if !ok { + return 0, nil, io.EOF + } + return binary.LittleEndian.Uint32(buf), buf[4:], nil +} + +func (s *Session16) RecvFrameData() (frameInfo, frameData []byte, err error) { + buf, ok := <-s.rawPkt + if !ok { + return nil, nil, io.EOF + } + return buf[0], buf[1], nil +} + +func (s *Session16) SessionRead(chID byte, cmd []byte) int { + if chID != 0 { + return s.handleCh1(cmd) + } + + // 0 01030800 command + version + // 4 00000000 frame num + // 8 ac880100 total size + // 12 6200 chunk seq + // 14 2000 tail (pkt header) size + // 16 cc00 size + // 18 0000 + // 20 01000000 fixed + + switch cmd[0] { + case 0x01: + var packetData [2][]byte + + switch cmd[1] { + case 0x03: + seq := binary.LittleEndian.Uint16(cmd[12:]) + if seq != s.waitSeq { + s.waitSeq = 0 + return msgMediaLost + } + if seq == 0 { + s.waitData = s.waitData[:0] + payloadSize := binary.LittleEndian.Uint32(cmd[8:]) + hdrSize := binary.LittleEndian.Uint16(cmd[14:]) + s.waitSize = int(hdrSize) + int(payloadSize) + } + + s.waitData = append(s.waitData, cmd[24:]...) + if n := len(s.waitData); n < s.waitSize { + s.waitSeq++ + return msgMediaChunk + } + + s.waitSeq = 0 + + payloadSize := binary.LittleEndian.Uint32(cmd[8:]) + packetData[0] = bytes.Clone(s.waitData[payloadSize:]) + packetData[1] = bytes.Clone(s.waitData[:payloadSize]) + + case 0x04: + data := cmd[24:] + hdrSize := binary.LittleEndian.Uint16(cmd[14:]) + packetData[0] = bytes.Clone(data[:hdrSize]) + packetData[1] = bytes.Clone(data[hdrSize:]) + + default: + return msgUnknown + } + + select { + case s.rawPkt <- packetData: + default: + return msgError + } + return msgMediaFrame + + case 0x00: + switch cmd[1] { + case 0x70: + _ = s.SessionWrite(0, s.msgAck0070(cmd)) + select { + case s.rawCmd <- append([]byte{}, cmd[24:]...): + default: + } + + return msgCommand + case 0x12: + _ = s.SessionWrite(0, s.msgAck0012(cmd)) + return msgDafang0012 + case 0x71: + return msgCommandAck + } + } + + return msgUnknown +} + +func (s *Session16) msgAck0070(msg28 []byte) []byte { + // <- 00700800010000000000000000000000340000007625a02f ... + // -> 00710800010000000000000000000000000000007625a02f + msg := s.Msg(msgHhrSize + cmdHdrSize) + + cmd := msg[msgHhrSize:] + copy(cmd, "\x00\x71") + copy(cmd[2:], msg28[2:6]) // same version and seq + copy(cmd[20:], msg28[20:24]) // same msg random + + return msg +} + +func (s *Session16) msgAck0012(msg28 []byte) []byte { + // <- 001208000000000000000000000000000c00000000000000 020000000100000001000000 + // -> 00130b000000000000000000000000001400000000000000 0200000001000000010000000000000000000000 + const dataSize = 20 + msg := s.Msg(msgHhrSize + cmdHdrSize + dataSize) + + cmd := msg[msgHhrSize:] + copy(cmd, "\x00\x13\x0b\x00") + cmd[16] = dataSize + + data := cmd[cmdHdrSize:] + copy(data, msg28[cmdHdrSize:]) + + return msg +} + +func (s *Session16) handleCh1(cmd []byte) int { + // Channel 1 used for two-way audio. It's important: + // - answer on 0000 command with exact config response (can't set simple proto) + // - send 0012 command at start + // - respond on every 0008 command for smooth playback + switch cid := string(cmd[:2]); cid { + case "\x00\x00": // client start + _ = s.SessionWrite(1, s.msgAck0000(cmd)) + _ = s.SessionWrite(1, s.msg0012()) + return msgClientStart + case "\x00\x07": // time sync without data + _ = s.SessionWrite(1, s.msgAck0007(cmd)) + return msgUnknown0007 + case "\x00\x08": // time sync with data + _ = s.SessionWrite(1, s.msgAck0008(cmd)) + return msgUnknown0008 + case "\x00\x13": // ack for 0012 + return msgUnknown0013 + } + return msgUnknown +} + +func (s *Session16) msgAck0000(msg28 []byte) []byte { + // <- 000008000000000000000000000000001a0200004f47c714 ... 00000000000000000100000004000000fb071f00000000000000000000000300 + // -> 00140b00000000000000000000000000200000004f47c714 00000000000000000100000004000000fb071f00000000000000000000000300 + const cmdDataSize = 32 + msg := s.Msg(msgHhrSize + cmdHdrSize + cmdDataSize) + + cmd := msg[msgHhrSize:] + copy(cmd, "\x00\x14\x0b\x00") + cmd[16] = cmdDataSize + copy(cmd[20:], msg28[20:24]) // request id (random) + + // Important to answer with same data. + data := cmd[cmdHdrSize:] + copy(data, msg28[len(msg28)-32:]) + return msg +} + +func (s *Session16) msg0012() []byte { + // -> 00120b000000000000000000000000000c00000000000000020000000100000001000000 + const dataSize = 12 + msg := s.Msg(msgHhrSize + cmdHdrSize + dataSize) + cmd := msg[msgHhrSize:] + + copy(cmd, "\x00\x12\x0b\x00") + cmd[16] = dataSize + data := cmd[cmdHdrSize:] + + data[0] = 2 + data[4] = 1 + data[9] = 1 + return msg +} + +func (s *Session16) msgAck0007(msg28 []byte) []byte { + // <- 000708000000000000000000000000000c00000001000000000000001c551f7a00000000 + // -> 010a0b00000000000000000000000000000000000100000000000000 + msg := s.Msg(msgHhrSize + 28) + cmd := msg[msgHhrSize:] + copy(cmd, "\x01\x0a\x0b\x00") + cmd[20] = 1 + return msg +} + +func (s *Session16) msgAck0008(msg28 []byte) []byte { + // <- 000808000000000000000000000000000000f9f0010000000200000050f31f7a + // -> 01090b0000000000000000000000000000000000010000000200000050f31f7a + msg := s.Msg(msgHhrSize + 28) + cmd := msg[msgHhrSize:] + copy(cmd, "\x01\x09\x0b\x00") + copy(cmd[20:], msg28[20:]) + return msg +} + +func (s *Session16) SessionWrite(chID byte, buf []byte) error { + switch chID { + case 0: + binary.LittleEndian.PutUint16(buf[6:], s.seqSendCh0) + s.seqSendCh0++ + case 1: + binary.LittleEndian.PutUint16(buf[6:], s.seqSendCh1) + s.seqSendCh1++ + buf[14] = 1 // channel + } + _, err := s.conn.Write(buf) + return err +} diff --git a/pkg/tutk/session25.go b/pkg/tutk/session25.go new file mode 100644 index 00000000..a12f52f3 --- /dev/null +++ b/pkg/tutk/session25.go @@ -0,0 +1,341 @@ +package tutk + +import ( + "bytes" + "encoding/binary" + "net" + "time" +) + +func NewSession25(conn net.Conn, sid []byte) *Session25 { + return &Session25{ + Session16: NewSession16(conn, sid), + rb: NewReorderBuffer(5), + } +} + +type Session25 struct { + *Session16 + + rb *ReorderBuffer + + seqSendCmd2 uint16 + seqSendCnt uint16 + + seqRecvPkt0 uint16 + seqRecvPkt1 uint16 + seqRecvCmd2 uint16 +} + +const cmdHdrSize25 = 28 + +func (s *Session25) SendIOCtrl(ctrlType uint32, ctrlData []byte) []byte { + size := msgHhrSize + cmdHdrSize25 + 4 + uint16(len(ctrlData)) + msg := s.Msg(size) + + // 0 0070 command + // 2 0b00 version + // 4 1000 seq + // 6 0076 ??? + cmd := msg[msgHhrSize:] + copy(cmd, "\x00\x70\x0b\x00") + binary.LittleEndian.PutUint16(cmd[4:], s.seqSendCmd1) + s.seqSendCmd1++ + + // 8 0070 command (second time) + // 10 0300 seq + // 12 0100 chunks count + // 14 0000 chunk seq (starts from 0) + // 16 5500 size + // 18 0000 random msg id (always 0) + // 20 03000000 seq (second time) + // 24 00000000 + // 28 01010000 ctrlType + cmd[9] = 0x70 + cmd[12] = 1 + binary.LittleEndian.PutUint16(cmd[16:], size-52) + + binary.LittleEndian.PutUint16(cmd[10:], s.seqSendCmd2) + binary.LittleEndian.PutUint16(cmd[20:], s.seqSendCmd2) + s.seqSendCmd2++ + + data := cmd[28:] + binary.LittleEndian.PutUint32(data, ctrlType) + copy(data[4:], ctrlData) + return msg +} + +func (s *Session25) SendFrameData(frameInfo, frameData []byte) []byte { + return nil +} + +func (s *Session25) SessionRead(chID byte, cmd []byte) (res int) { + if chID != 0 { + return s.handleCh1(cmd) + } + + switch cmd[0] { + case 0x03, 0x05, 0x07: + for i := 0; cmd != nil; i++ { + res = s.handleChunk(cmd, i == 0) + cmd = s.rb.Pop() + } + return + + case 0x00: + _ = s.SessionWrite(0, s.msgAckCounters()) + s.seqRecvCmd2 = binary.LittleEndian.Uint16(cmd[2:]) + + switch cmd[1] { + case 0x10: + return msgUnknown0010 // unknown + case 0x21: + return msgClientStartAck2 + case 0x70: + select { + case s.rawCmd <- cmd[28:]: + default: + } + return msgCommand // cmd from camera + case 0x71: + return msgCommandAck + } + + case 0x09: + // off sample + // 0 09000b00 cmd1 + // 4 0d000000 seqCmd1 + // 12 0000 seqRecvCmd2 + seq := binary.LittleEndian.Uint16(cmd[12:]) + if s.seqSendCmd1 > seq { + return msgCommandAck + } + return msgCounters + + case 0x0a: + // seq sample + // 0 0a080b00 + // 4 03000000 + // 8 e2043200 + // 12 01000000 + _ = s.SessionWrite(0, s.msgAck0A08(cmd)) + return msgUnknown0a08 + } + + return msgUnknown +} + +func (s *Session25) handleChunk(cmd []byte, checkSeq bool) int { + var cmd2 []byte + + flags := cmd[1] + if flags&0b1000 == 0 { + // off sample + // 0 0700 command + // 2 0b00 version + // 4 2700 seq + // 6 0000 ??? + // 8 0700 command (second time) + // 10 1400 seq + // 12 1300 chunks count per this frame + // 14 1100 chunk seq, starts from 0 (0x20 for last chunk) + // 16 0004 frame data size + // 18 0000 random msg id (always 0) + // 20 02000000 previous frame seq, starts from 0 + // 24 03000000 current frame seq, starts from 1 + cmd2 = cmd[8:] + } else { + // off sample + // 0 070d0b00 + // 4 30000000 + // 8 5c965500 ??? + // 12 ffff0000 ??? + // 16 0701 fixed command + // 18 190001002000a802000006000000070000000 + cmd2 = cmd[16:] + } + + seq := binary.LittleEndian.Uint16(cmd2[2:]) + + if checkSeq { + if s.rb.Check(seq) { + s.rb.Next() + } else { + s.rb.Push(seq, cmd) + return msgMediaReorder + } + } + + // Check if this is first chunk for frame. + // Handle protocol bug "0x20 chunk seq for last chunk" and sometimes + // "0x20 chunk seq for first chunk if only one chunk". + if binary.LittleEndian.Uint16(cmd2[6:]) == 0 || binary.LittleEndian.Uint16(cmd2[4:]) == 1 { + s.waitData = s.waitData[:0] + s.waitSeq = seq + } else if seq != s.waitSeq { + return msgMediaLost + } + + s.waitData = append(s.waitData, cmd2[20:]...) + + if flags&0b0001 == 0 { + s.waitSeq++ + return msgMediaChunk + } + + s.seqRecvPkt1 = seq + _ = s.SessionWrite(0, s.msgAckCounters()) + + n := len(s.waitData) - 32 + packetData := [2][]byte{bytes.Clone(s.waitData[n:]), bytes.Clone(s.waitData[:n])} + + select { + case s.rawPkt <- packetData: + default: + return msgError + } + return msgMediaFrame +} + +func (s *Session25) msgAckCounters() []byte { + msg := s.Msg(msgHhrSize + cmdHdrSize) + + // off sample + // 0 09000b00 cmd1 + // 4 2700 seqCmd1 + // 6 0000 + // 8 1300 seqRecvPkt0 + // 10 2600 seqRecvPkt1 + // 12 0400 seqRecvCmd2 + // 14 00000000 + // 18 1400 seqSendCnt + // 20 d91a random + // 22 0000 + cmd := msg[msgHhrSize:] + copy(cmd, "\x09\x00\x0b\x00") + + binary.LittleEndian.PutUint16(cmd[4:], s.seqSendCmd1) + s.seqSendCmd1++ + + // seqRecvPkt0 stores previous value of seqRecvPkt1 + // don't understand why this needs + binary.LittleEndian.PutUint16(cmd[8:], s.seqRecvPkt0) + s.seqRecvPkt0 = s.seqRecvPkt1 + binary.LittleEndian.PutUint16(cmd[10:], s.seqRecvPkt1) + binary.LittleEndian.PutUint16(cmd[12:], s.seqRecvCmd2) + + binary.LittleEndian.PutUint16(cmd[18:], s.seqSendCnt) + s.seqSendCnt++ + binary.LittleEndian.PutUint16(cmd[20:], uint16(time.Now().UnixMilli())) + return msg +} + +func (s *Session25) handleCh1(cmd []byte) int { + // Channel 1 used for two-way audio. It's important: + // - answer on 0000 command with exact config response (can't set simple proto) + // - send 0012 command at start + // - respond on every 0008 command for smooth playback + switch cid := string(cmd[:2]); cid { + case "\x00\x00": // client start + return msgClientStart + case "\x00\x07": // time sync without data + _ = s.SessionWrite(1, s.msgAck0007(cmd)) + return msgUnknown0007 + case "\x00\x20": // client start2 + _ = s.SessionWrite(1, s.msgAck0020(cmd)) + return msgClientStart2 + case "\x09\x00": + return msgUnknown0900 + case "\x0a\x08": + return msgUnknown0a08 + } + return msgUnknown +} + +func (s *Session25) msgAck0020(msg28 []byte) []byte { + const cmdDataSize = 36 + + msg := s.Msg(msgHhrSize + cmdHdrSize25 + cmdDataSize) + + cmd := msg[msgHhrSize:] + copy(cmd, "\x00\x21\x0b\x00") + cmd[16] = cmdDataSize + copy(cmd[20:], msg28[20:24]) // request id (random) + + // 0 00000000 + // 4 00010001 + // 8 01000000 + // 12 04000000 + // 16 fb071f00 + // 20 00000000 + // 24 00000000 + // 28 00000300 + // 32 01000000 + data := cmd[cmdHdrSize25:] + data[5] = 1 + data[7] = 1 + data[8] = 1 + data[12] = 4 + copy(data[16:], "\xfb\x07\x1f\x00") + data[30] = 3 + data[32] = 1 + return msg +} + +func (s *Session25) msgAck0A08(msg28 []byte) []byte { + // <- 0a080b005b0000000b51590002000000 + // -> 0b000b00000001000b5103000300000000000000 + msg := s.Msg(msgHhrSize + 20) + cmd := msg[msgHhrSize:] + copy(cmd, "\x0b\x00\x0b\x00") + copy(cmd[8:], msg28[8:10]) + return msg +} + +// ReorderBuffer used for UDP incoming data. Because the order of the packets may be mixed up. +type ReorderBuffer struct { + buf map[uint16][]byte + seq uint16 + size int +} + +func NewReorderBuffer(size int) *ReorderBuffer { + return &ReorderBuffer{buf: make(map[uint16][]byte), size: size} +} + +// Check return OK if this is the seq we are waiting for. +func (r *ReorderBuffer) Check(seq uint16) (ok bool) { + return seq == r.seq +} + +func (r *ReorderBuffer) Next() { + r.seq++ +} + +// Available return how much free slots is in the buffer. +func (r *ReorderBuffer) Available() int { + return r.size - len(r.buf) +} + +// Push new item to buffer. Important! There is no buffer full check here. +func (r *ReorderBuffer) Push(seq uint16, data []byte) { + //log.Printf("push seq=%d wait=%d", seq, r.seq) + r.buf[seq] = bytes.Clone(data) +} + +// Pop latest item from buffer. OK - if items wasn't dropped. +func (r *ReorderBuffer) Pop() []byte { + for { + if data := r.buf[r.seq]; data != nil { + delete(r.buf, r.seq) + r.Next() + //log.Printf("pop seq=%d", r.seq) + return data + } + if r.Available() > 0 { + return nil + } + //log.Printf("drop seq=%d", r.seq) + r.Next() // drop item + } +} diff --git a/pkg/xiaomi/crypto/crypto.go b/pkg/xiaomi/crypto/crypto.go new file mode 100644 index 00000000..16d6d1bc --- /dev/null +++ b/pkg/xiaomi/crypto/crypto.go @@ -0,0 +1,68 @@ +package crypto + +import ( + "crypto/rand" + "encoding/hex" + + "golang.org/x/crypto/chacha20" + "golang.org/x/crypto/nacl/box" +) + +func GenerateKey() ([]byte, []byte, error) { + public, private, err := box.GenerateKey(rand.Reader) + if err != nil { + return nil, nil, err + } + return public[:], private[:], err +} + +func CalcSharedKey(devicePublicB64, clientPrivateB64 string) ([]byte, error) { + var sharedKey, publicKey, privateKey [32]byte + if _, err := hex.Decode(publicKey[:], []byte(devicePublicB64)); err != nil { + return nil, err + } + if _, err := hex.Decode(privateKey[:], []byte(clientPrivateB64)); err != nil { + return nil, err + } + box.Precompute(&sharedKey, &publicKey, &privateKey) + return sharedKey[:], nil +} + +func Encode(src, key32 []byte) ([]byte, error) { + dst := make([]byte, len(src)+8) + + if _, err := rand.Read(dst[:8]); err != nil { + return nil, err + } + + nonce12 := make([]byte, 12) + copy(nonce12[4:], dst[:8]) + + c, err := chacha20.NewUnauthenticatedCipher(key32, nonce12) + if err != nil { + return nil, err + } + + c.XORKeyStream(dst[8:], src) + + return dst, nil +} + +func Decode(src, key32 []byte) ([]byte, error) { + return DecodeNonce(src[8:], src[:8], key32) +} + +func DecodeNonce(src, nonce8, key32 []byte) ([]byte, error) { + nonce12 := make([]byte, 12) + copy(nonce12[4:], nonce8) + + c, err := chacha20.NewUnauthenticatedCipher(key32, nonce12) + if err != nil { + return nil, err + } + + dst := make([]byte, len(src)) + c.XORKeyStream(dst, src) + + return dst, nil +} diff --git a/pkg/xiaomi/legacy/client.go b/pkg/xiaomi/legacy/client.go new file mode 100644 index 00000000..cf84b6fe --- /dev/null +++ b/pkg/xiaomi/legacy/client.go @@ -0,0 +1,227 @@ +package legacy + +import ( + "encoding/binary" + "errors" + "fmt" + "net/url" + + "github.com/AlexxIT/go2rtc/pkg/tutk" + "github.com/AlexxIT/go2rtc/pkg/xiaomi/crypto" +) + +func NewClient(rawURL string) (*Client, error) { + u, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + + query := u.Query() + model := query.Get("model") + + var username, password string + var key []byte + + if query.Has("sign") { + // Legacy with encryption + key, err = crypto.CalcSharedKey(query.Get("device_public"), query.Get("client_private")) + if err != nil { + return nil, err + } + + username = fmt.Sprintf( + `{"public_key":"%s","sign":"%s","account":"admin"}`, + query.Get("client_public"), query.Get("sign"), + ) + } else if model == ModelXiaobai { + username = "admin" + password = query.Get("password") + } else if model == ModelXiaofang { + username = "admin" + } else { + return nil, fmt.Errorf("xiaomi: unsupported model: %s", model) + } + + conn, err := tutk.Dial(u.Host, query.Get("uid"), username, password) + if err != nil { + return nil, err + } + + if model == ModelXiaofang { + err = xiaofangLogin(conn, query.Get("password")) + if err != nil { + _ = conn.Close() + return nil, err + } + } + + c := &Client{ + Conn: conn, + key: key, + model: model, + } + + return c, nil +} + +func xiaofangLogin(conn *tutk.Conn, password string) error { + data := tutk.ICAM(0x0400be) // ask login + if err := conn.WriteCommand(0x0100, data); err != nil { + return err + } + + _, data, err := conn.ReadCommand() // login request + if err != nil { + return err + } + + enc := data[24:] // data[23] == 3 + tutk.XXTEADecrypt(enc, enc, []byte(password)) + + enc = append(enc, 0, 0, 0, 0, 1, 1, 1) + data = tutk.ICAM(0x0400c0, enc...) // login response + if err = conn.WriteCommand(0x0100, data); err != nil { + return err + } + + _, data, err = conn.ReadCommand() + if err != nil { + return err + } + return nil +} + +type Client struct { + *tutk.Conn + key []byte + model string +} + +func (c *Client) Version() string { + return fmt.Sprintf("%s (%s)", c.Conn.Version(), c.model) +} + +func (c *Client) ReadPacket() (hdr, payload []byte, err error) { + hdr, payload, err = c.Conn.ReadPacket() + if err != nil { + return + } + if c.key != nil { + switch hdr[0] { + case tutk.CodecH264, tutk.CodecH265: + payload, err = DecodeVideo(payload, c.key) + if err != nil { + return + } + case tutk.CodecAAC: + payload, err = crypto.Decode(payload, c.key) + if err != nil { + return + } + } + } + return +} + +func (c *Client) StartMedia(video, audio string) error { + switch c.model { + case ModelAqaraG2: + return c.WriteCommand(0x01ff, []byte(`{}`)) + + case ModelXiaobai: + // 00030000 7b7d audio on + // 01030000 7b7d audio off + if err := c.WriteCommand(0x0300, []byte(`{}`)); err != nil { + return err + } + + var b byte + switch video { + case "", "fhd": + b = 1 + case "hd": + b = 2 + case "sd": + b = 4 + case "auto": + b = 0xff + } + // 20030000 0000000001000000 fhd (1920x1080) + // 20030000 0000000002000000 hd (1280x720) + // 20030000 0000000004000000 low (640x360) + // 20030000 00000000ff000000 auto (1920x1080) + if err := c.WriteCommand(0x0320, []byte{0, 0, 0, 0, b, 0, 0, 0}); err != nil { + return err + } + + // ff010000 7b7d video tart + // ff020000 7b7d video stop + return c.WriteCommand(0x01ff, []byte(`{}`)) + + case ModelXiaofang: + // 00010000 4943414d 95010400000000000000000600000000000000d20400005a07 - 90k bitrate + // 00010000 4943414d 95010400000000000000000600000000000000d20400001e07 - 30k bitrate + //var b byte + //switch video { + //case "", "hd": + // b = 0x5a // bitrate 90k + //case "sd": + // b = 0x1e // bitrate 30k + //} + //data := tutk.ICAM(0x040195, 0xd2, 4, 0, 0, b, 7) + //if err := c.WriteCommand(0x100, data); err != nil { + // return err + //} + } + + return nil +} + +func (c *Client) StopMedia() error { + return errors.Join( + c.WriteCommand(0x02ff, []byte(`{}`)), + c.WriteCommand(0x02ff, make([]byte, 8)), + ) +} + +func DecodeVideo(data, key []byte) ([]byte, error) { + if string(data[:4]) == "\x00\x00\x00\x01" || data[8] == 0 { + return data, nil + } + + if data[8] != 1 { + // Support could be added, but I haven't seen such cameras. + return nil, fmt.Errorf("xiaomi: unsupported encryption") + } + + nonce8 := data[:8] + i1 := binary.LittleEndian.Uint16(data[9:]) + i2 := binary.LittleEndian.Uint16(data[13:]) + data = data[17:] + src := data[i1 : i1+i2] + + for i := 32; i+16 < len(src); i += 160 { + dst, err := crypto.DecodeNonce(src[i:i+16], nonce8, key) + if err != nil { + return nil, err + } + copy(src[i:], dst) // copy result in same place + } + + return data, nil +} + +const ( + ModelAqaraG2 = "lumi.camera.gwagl01" + ModelLoockV1 = "loock.cateye.v01" + ModelXiaobai = "chuangmi.camera.xiaobai" + ModelXiaofang = "isa.camera.isc5" +) + +func Supported(model string) bool { + switch model { + case ModelAqaraG2, ModelLoockV1, ModelXiaobai, ModelXiaofang: + return true + } + return false +} diff --git a/pkg/xiaomi/legacy/producer.go b/pkg/xiaomi/legacy/producer.go new file mode 100644 index 00000000..6669d837 --- /dev/null +++ b/pkg/xiaomi/legacy/producer.go @@ -0,0 +1,217 @@ +package legacy + +import ( + "net/url" + "time" + + "github.com/AlexxIT/go2rtc/pkg/aac" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/h264" + "github.com/AlexxIT/go2rtc/pkg/h264/annexb" + "github.com/AlexxIT/go2rtc/pkg/h265" + "github.com/AlexxIT/go2rtc/pkg/tutk" + "github.com/pion/rtp" +) + +func Dial(rawURL string) (*Producer, error) { + client, err := NewClient(rawURL) + if err != nil { + return nil, err + } + + u, _ := url.Parse(rawURL) + query := u.Query() + + err = client.StartMedia(query.Get("subtype"), "") + if err != nil { + _ = client.Close() + return nil, err + } + + medias, err := probe(client) + if err != nil { + _ = client.Close() + return nil, err + } + + c := &Producer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "xiaomi/legacy", + Protocol: "tutk+udp", + RemoteAddr: client.RemoteAddr().String(), + UserAgent: client.Version(), + Medias: medias, + Transport: client, + }, + client: client, + } + return c, nil +} + +type Producer struct { + core.Connection + client *Client +} + +const codecXiaobaiPCMA = 1 // chuangmi.camera.xiaobai + +func probe(client *Client) ([]*core.Media, error) { + _ = client.SetDeadline(time.Now().Add(15 * time.Second)) + + var vcodec, acodec *core.Codec + + for { + // 0 5000 + // 2 0000 codec params + // 4 01 active clients + // 5 34 unknown const + // 6 0600 unknown seq(s) + // 8 80026801 unknown fixed + // 12 ed8d5c69 time in sec + // 16 4c03 time in 1/1000 + // 18 0000 + hdr, payload, err := client.ReadPacket() + if err != nil { + return nil, err + } + + switch codec := hdr[0]; codec { + case tutk.CodecH264, tutk.CodecH265: + if vcodec == nil { + avcc := annexb.EncodeToAVCC(payload) + if codec == tutk.CodecH264 { + if h264.NALUType(avcc) == h264.NALUTypeSPS { + vcodec = h264.AVCCToCodec(avcc) + vcodec.FmtpLine = "" + } + } else { + if h265.NALUType(avcc) == h265.NALUTypeVPS { + vcodec = h265.AVCCToCodec(avcc) + } + } + } + case tutk.CodecPCMA, codecXiaobaiPCMA: + if acodec == nil { + acodec = &core.Codec{Name: core.CodecPCMA, ClockRate: 8000} + } + case tutk.CodecPCML: + if acodec == nil { + acodec = &core.Codec{Name: core.CodecPCML, ClockRate: 8000} + } + case tutk.CodecAAC: + if acodec == nil { + acodec = aac.ADTSToCodec(payload) + if acodec != nil { + acodec.PayloadType = core.PayloadTypeRAW + } + } + } + + if vcodec != nil && acodec != nil { + break + } + } + + medias := []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{vcodec}, + }, + { + Kind: core.KindAudio, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{acodec}, + }, + } + return medias, nil +} + +func (c *Producer) Protocol() string { + return "tutk+udp" +} + +func (c *Producer) Start() error { + var audioTS uint32 + var videoSeq, audioSeq uint16 + + for { + _ = c.client.SetDeadline(time.Now().Add(5 * time.Second)) + hdr, payload, err := c.client.ReadPacket() + if err != nil { + return err + } + + n := len(payload) + c.Recv += n + + // TODO: rewrite this + var name string + var pkt *core.Packet + + switch codec := hdr[0]; codec { + case tutk.CodecH264, tutk.CodecH265: + pkt = &core.Packet{ + Header: rtp.Header{ + SequenceNumber: videoSeq, + Timestamp: core.Now90000(), + }, + Payload: annexb.EncodeToAVCC(payload), + } + videoSeq++ + + if codec == tutk.CodecH264 { + name = core.CodecH264 + } else { + name = core.CodecH265 + } + + case tutk.CodecPCMA, tutk.CodecPCML, codecXiaobaiPCMA: + pkt = &core.Packet{ + Header: rtp.Header{ + Version: 2, + Marker: true, + SequenceNumber: audioSeq, + Timestamp: audioTS, + }, + Payload: payload, + } + audioSeq++ + + switch codec { + case tutk.CodecPCMA, codecXiaobaiPCMA: + name = core.CodecPCMA + audioTS += uint32(n) + case tutk.CodecPCML: + name = core.CodecPCML + audioTS += uint32(n / 2) // because 16bit + } + + case tutk.CodecAAC: + pkt = &core.Packet{ + Header: rtp.Header{ + SequenceNumber: audioSeq, + Timestamp: audioTS, + }, + Payload: payload, + } + audioSeq++ + + name = core.CodecAAC + audioTS += 1024 + } + + for _, recv := range c.Receivers { + if recv.Codec.Name == name { + recv.WriteRTP(pkt) + break + } + } + } +} + +func (c *Producer) Stop() error { + _ = c.client.StopMedia() + return c.Connection.Stop() +} diff --git a/pkg/xiaomi/backchannel.go b/pkg/xiaomi/miss/backchannel.go similarity index 76% rename from pkg/xiaomi/backchannel.go rename to pkg/xiaomi/miss/backchannel.go index 17242b8e..02ea3bb1 100644 --- a/pkg/xiaomi/backchannel.go +++ b/pkg/xiaomi/miss/backchannel.go @@ -1,4 +1,4 @@ -package xiaomi +package miss import ( "time" @@ -6,12 +6,11 @@ import ( "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/opus" "github.com/AlexxIT/go2rtc/pkg/pcm" - "github.com/AlexxIT/go2rtc/pkg/xiaomi/miss" "github.com/pion/rtp" ) func (p *Producer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error { - if err := p.client.SpeakerStart(); err != nil { + if err := p.client.StartSpeaker(); err != nil { return err } // TODO: check this!!! @@ -23,8 +22,7 @@ func (p *Producer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv case core.CodecPCMA: var buf []byte - switch p.model { - case "isa.camera.hlc6", "isa.camera.df3": + if p.client.SpeakerCodec() == codecPCM { dst := &core.Codec{Name: core.CodecPCML, ClockRate: 8000} transcode := pcm.Transcode(dst, track.Codec) @@ -33,23 +31,23 @@ func (p *Producer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv const size = 2 * 8000 * 0.040 // 16bit 40ms for len(buf) >= size { p.Send += size - _ = p.client.WriteAudio(miss.CodecPCM, buf[:size]) + _ = p.client.WriteAudio(codecPCM, buf[:size]) buf = buf[size:] } } - default: + } else { sender.Handler = func(pkt *rtp.Packet) { buf = append(buf, pkt.Payload...) const size = 8000 * 0.040 // 8bit 40 ms for len(buf) >= size { p.Send += size - _ = p.client.WriteAudio(miss.CodecPCMA, buf[:size]) + _ = p.client.WriteAudio(codecPCMA, buf[:size]) buf = buf[size:] } } } case core.CodecOpus: - if p.model == "chuangmi.camera.72ac1" { + if p.client.SpeakerCodec() == codecOPUS { var buf []byte sender.Handler = func(pkt *rtp.Packet) { if buf == nil { @@ -58,14 +56,14 @@ func (p *Producer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv // convert two 20ms to one 40ms buf = opus.JoinFrames(buf, pkt.Payload) p.Send += len(buf) - _ = p.client.WriteAudio(miss.CodecOPUS, buf) + _ = p.client.WriteAudio(codecOPUS, buf) buf = nil } } } else { sender.Handler = func(pkt *rtp.Packet) { p.Send += len(pkt.Payload) - _ = p.client.WriteAudio(miss.CodecOPUS, pkt.Payload) + _ = p.client.WriteAudio(codecOPUS, pkt.Payload) } } } diff --git a/pkg/xiaomi/miss/client.go b/pkg/xiaomi/miss/client.go index 560f9f49..6eaa06cf 100644 --- a/pkg/xiaomi/miss/client.go +++ b/pkg/xiaomi/miss/client.go @@ -2,96 +2,83 @@ package miss import ( "bytes" - "crypto/rand" "encoding/binary" "encoding/hex" + "errors" "fmt" "net" "net/url" "time" - "github.com/AlexxIT/go2rtc/pkg/xiaomi/cs2" - "github.com/AlexxIT/go2rtc/pkg/xiaomi/tutk" - "golang.org/x/crypto/chacha20" - "golang.org/x/crypto/nacl/box" + "github.com/AlexxIT/go2rtc/pkg/tutk" + "github.com/AlexxIT/go2rtc/pkg/xiaomi/crypto" + "github.com/AlexxIT/go2rtc/pkg/xiaomi/miss/cs2" ) -func Dial(rawURL string) (*Client, error) { - u, err := url.Parse(rawURL) - if err != nil { - return nil, err - } - - query := u.Query() - - c := &Client{} - - c.key, err = calcSharedKey(query.Get("device_public"), query.Get("client_private")) - if err != nil { - return nil, err - } - - switch s := query.Get("vendor"); s { - case "cs2": - c.conn, err = cs2.Dial(u.Host, query.Get("transport")) - case "tutk": - c.conn, err = tutk.Dial(u.Host, query.Get("uid"), query.Get("model")) - default: - return nil, fmt.Errorf("miss: unsupported vendor %s", s) - } - - if err != nil { - return nil, err - } - - err = c.login(query.Get("client_public"), query.Get("sign")) - if err != nil { - _ = c.conn.Close() - return nil, err - } - - return c, nil -} - const ( - CodecH264 = 4 - CodecH265 = 5 - CodecPCM = 1024 - CodecPCMU = 1026 - CodecPCMA = 1027 - CodecOPUS = 1032 + codecH264 = 4 + codecH265 = 5 + codecPCM = 1024 + codecPCMU = 1026 + codecPCMA = 1027 + codecOPUS = 1032 ) type Conn interface { Protocol() string - ReadCommand() (cmd uint16, data []byte, err error) - WriteCommand(cmd uint16, data []byte) error - ReadPacket() ([]byte, error) - WritePacket(data []byte) error + Version() string + ReadCommand() (cmd uint32, data []byte, err error) + WriteCommand(cmd uint32, data []byte) error + ReadPacket() (hdr, payload []byte, err error) + WritePacket(hdr, payload []byte) error RemoteAddr() net.Addr SetDeadline(t time.Time) error Close() error } +func NewClient(rawURL string) (*Client, error) { + u, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + + // 1. Check if we can create shared key. + query := u.Query() + key, err := crypto.CalcSharedKey(query.Get("device_public"), query.Get("client_private")) + if err != nil { + return nil, err + } + + model := query.Get("model") + + // 2. Check if this vendor supported. + var conn Conn + switch s := query.Get("vendor"); s { + case "cs2": + conn, err = cs2.Dial(u.Host, query.Get("transport")) + case "tutk": + conn, err = tutk.Dial(u.Host, query.Get("uid"), "Miss", "client") + default: + err = fmt.Errorf("miss: unsupported vendor %s", s) + } + + if err != nil { + return nil, err + } + + err = login(conn, query.Get("client_public"), query.Get("sign")) + if err != nil { + _ = conn.Close() + return nil, err + } + + return &Client{Conn: conn, key: key, model: model}, nil +} + type Client struct { - conn Conn - key []byte -} - -func (c *Client) Protocol() string { - return c.conn.Protocol() -} - -func (c *Client) RemoteAddr() net.Addr { - return c.conn.RemoteAddr() -} - -func (c *Client) SetDeadline(t time.Time) error { - return c.conn.SetDeadline(t) -} - -func (c *Client) Close() error { - return c.conn.Close() + Conn + key []byte + model string } const ( @@ -117,13 +104,13 @@ const ( cmdEncoded = 0x1001 ) -func (c *Client) login(clientPublic, sign string) error { +func login(conn Conn, clientPublic, sign string) error { s := fmt.Sprintf(`{"public_key":"%s","sign":"%s","uuid":"","support_encrypt":0}`, clientPublic, sign) - if err := c.conn.WriteCommand(cmdAuthReq, []byte(s)); err != nil { + if err := conn.WriteCommand(cmdAuthReq, []byte(s)); err != nil { return err } - _, data, err := c.conn.ReadCommand() + _, data, err := conn.ReadCommand() if err != nil { return err } @@ -135,129 +122,148 @@ func (c *Client) login(clientPublic, sign string) error { return nil } +func (c *Client) Version() string { + return fmt.Sprintf("%s (%s)", c.Conn.Version(), c.model) +} + func (c *Client) WriteCommand(data []byte) error { - data, err := encode(c.key, data) + data, err := crypto.Encode(data, c.key) if err != nil { return err } - return c.conn.WriteCommand(cmdEncoded, data) + return c.Conn.WriteCommand(cmdEncoded, data) } -func (c *Client) VideoStart(channel, quality, audio uint8) error { +const ( + ModelDafang = "isa.camera.df3" + ModelLoockV2 = "loock.cateye.v02" + ModelC200 = "chuangmi.camera.046c04" + ModelC300 = "chuangmi.camera.72ac1" +) + +func (c *Client) StartMedia(channel, quality, audio string) error { + switch c.model { + case ModelDafang: + var q, a byte + if quality == "sd" { + q = 1 // 0 - hd, 1 - sd, default - hd + } + if audio != "0" { + a = 1 // 0 - off, 1 - on, default - on + } + + return errors.Join( + c.WriteCommand(dafangVideoQuality(q)), + c.WriteCommand(dafangVideoStart(1, a)), + ) + } + + // 0 - auto, 1 - sd, 2 - hd, default - hd + switch quality { + case "", "hd": + // Some models have broken codec settings in quality 3. + // Some models have low quality in quality 2. + // Different models require different default quality settings. + switch c.model { + case ModelC200, ModelC300: + quality = "3" + default: + quality = "2" + } + case "sd": + quality = "1" + case "auto": + quality = "0" + } + + if audio == "" { + audio = "1" + } + data := binary.BigEndian.AppendUint32(nil, cmdVideoStart) - if channel == 0 { - data = fmt.Appendf(data, `{"videoquality":%d,"enableaudio":%d}`, quality, audio) + if channel == "" { + data = fmt.Appendf(data, `{"videoquality":%s,"enableaudio":%s}`, quality, audio) } else { - data = fmt.Appendf(data, `{"videoquality":-1,"videoquality2":%d,"enableaudio":%d}`, quality, audio) + data = fmt.Appendf(data, `{"videoquality":-1,"videoquality2":%s,"enableaudio":%s}`, quality, audio) } return c.WriteCommand(data) } -func (c *Client) AudioStart() error { +func (c *Client) StopMedia() error { + data := binary.BigEndian.AppendUint32(nil, cmdVideoStop) + return c.WriteCommand(data) +} + +func (c *Client) StartAudio() error { data := binary.BigEndian.AppendUint32(nil, cmdAudioStart) return c.WriteCommand(data) } -func (c *Client) SpeakerStart() error { +func (c *Client) StartSpeaker() error { data := binary.BigEndian.AppendUint32(nil, cmdSpeakerStartReq) return c.WriteCommand(data) } +// SpeakerCodec if the camera model has a non-standard two-way codec. +func (c *Client) SpeakerCodec() uint32 { + switch c.model { + case ModelDafang, "isa.camera.hlc6": + return codecPCM + case "chuangmi.camera.72ac1": + return codecOPUS + } + return 0 +} + +const hdrSize = 32 + func (c *Client) ReadPacket() (*Packet, error) { - data, err := c.conn.ReadPacket() + hdr, payload, err := c.Conn.ReadPacket() if err != nil { return nil, fmt.Errorf("miss: read media: %w", err) } - return unmarshalPacket(c.key, data) -} -func unmarshalPacket(key, b []byte) (*Packet, error) { - n := uint32(len(b)) - - if n < 32 { + if len(hdr) < hdrSize { return nil, fmt.Errorf("miss: packet header too small") } - if l := binary.LittleEndian.Uint32(b); l+32 != n { - return nil, fmt.Errorf("miss: packet payload has wrong length") - } - - payload, err := decode(key, b[32:]) + payload, err = crypto.Decode(payload, c.key) if err != nil { return nil, err } - return &Packet{ - CodecID: binary.LittleEndian.Uint32(b[4:]), - Sequence: binary.LittleEndian.Uint32(b[8:]), - Flags: binary.LittleEndian.Uint32(b[12:]), - Timestamp: binary.LittleEndian.Uint64(b[16:]), - Payload: payload, - }, nil + pkt := &Packet{ + CodecID: binary.LittleEndian.Uint32(hdr[4:]), + Sequence: binary.LittleEndian.Uint32(hdr[8:]), + Flags: binary.LittleEndian.Uint32(hdr[12:]), + Payload: payload, + } + + switch c.model { + case ModelDafang, ModelLoockV2: + // Dafang has ts in sec + // LoockV2 has ts in msec for video, but zero ts for audio + pkt.Timestamp = uint64(time.Now().UnixMilli()) + default: + pkt.Timestamp = binary.LittleEndian.Uint64(hdr[16:]) + } + + return pkt, nil } func (c *Client) WriteAudio(codecID uint32, payload []byte) error { - payload, err := encode(c.key, payload) // new payload will have new size! + payload, err := crypto.Encode(payload, c.key) // new payload will have new size! if err != nil { return err } - const hdrSize = 32 n := uint32(len(payload)) - data := make([]byte, hdrSize+n) - binary.LittleEndian.PutUint32(data, n) - binary.LittleEndian.PutUint32(data[4:], codecID) - binary.LittleEndian.PutUint64(data[16:], uint64(time.Now().UnixMilli())) // not really necessary - copy(data[hdrSize:], payload) - return c.conn.WritePacket(data) -} - -func calcSharedKey(devicePublic, clientPrivate string) ([]byte, error) { - var sharedKey, publicKey, privateKey [32]byte - if _, err := hex.Decode(publicKey[:], []byte(devicePublic)); err != nil { - return nil, err - } - if _, err := hex.Decode(privateKey[:], []byte(clientPrivate)); err != nil { - return nil, err - } - box.Precompute(&sharedKey, &publicKey, &privateKey) - return sharedKey[:], nil -} - -func encode(key, src []byte) ([]byte, error) { - dst := make([]byte, len(src)+8) - - if _, err := rand.Read(dst[:8]); err != nil { - return nil, err - } - - nonce := make([]byte, 12) - copy(nonce[4:], dst[:8]) - - c, err := chacha20.NewUnauthenticatedCipher(key, nonce) - if err != nil { - return nil, err - } - - c.XORKeyStream(dst[8:], src) - - return dst, nil -} - -func decode(key, src []byte) ([]byte, error) { - nonce := make([]byte, 12) - copy(nonce[4:], src[:8]) - - c, err := chacha20.NewUnauthenticatedCipher(key, nonce) - if err != nil { - return nil, err - } - - dst := make([]byte, len(src)-8) - c.XORKeyStream(dst, src[8:]) - - return dst, nil + header := make([]byte, hdrSize) + binary.LittleEndian.PutUint32(header, n) + binary.LittleEndian.PutUint32(header[4:], codecID) + binary.LittleEndian.PutUint64(header[16:], uint64(time.Now().UnixMilli())) // not really necessary + return c.Conn.WritePacket(header, payload) } type Packet struct { @@ -271,10 +277,40 @@ type Packet struct { Payload []byte } -func GenerateKey() ([]byte, []byte, error) { - public, private, err := box.GenerateKey(rand.Reader) - if err != nil { - return nil, nil, err - } - return public[:], private[:], err +func dafangRaw(cmd uint32, args ...byte) []byte { + payload := tutk.ICAM(cmd, args...) + + data := make([]byte, 4+len(payload)*2) + copy(data, "\x7f\xff\xff\xff") + hex.Encode(data[4:], payload) + return data } + +// DafangVideoQuality 0 - hd, 1 - sd +func dafangVideoQuality(quality uint8) []byte { + return dafangRaw(0xff07d5, quality) +} + +func dafangVideoStart(video, audio uint8) []byte { + return dafangRaw(0xff07d8, video, audio) +} + +//func dafangLeft() []byte { +// return dafangRaw(0xff2404, 2, 0, 5) +//} +// +//func dafangRight() []byte { +// return dafangRaw(0xff2404, 1, 0, 5) +//} +// +//func dafangUp() []byte { +// return dafangRaw(0xff2404, 0, 2, 5) +//} +// +//func dafangDown() []byte { +// return dafangRaw(0xff2404, 0, 1, 5) +//} +// +//func dafangStop() []byte { +// return dafangRaw(0xff2404, 0, 0, 5) +//} diff --git a/pkg/xiaomi/cs2/conn.go b/pkg/xiaomi/miss/cs2/conn.go similarity index 93% rename from pkg/xiaomi/cs2/conn.go rename to pkg/xiaomi/miss/cs2/conn.go index 198b3beb..670d2b91 100644 --- a/pkg/xiaomi/cs2/conn.go +++ b/pkg/xiaomi/miss/cs2/conn.go @@ -201,6 +201,10 @@ func (c *Conn) Protocol() string { return "cs2+udp" } +func (c *Conn) Version() string { + return "CS2" +} + func (c *Conn) RemoteAddr() net.Addr { return c.conn.RemoteAddr() } @@ -220,21 +224,19 @@ func (c *Conn) Error() error { return io.EOF } -func (c *Conn) ReadCommand() (cmd uint16, data []byte, err error) { +func (c *Conn) ReadCommand() (cmd uint32, data []byte, err error) { buf, ok := <-c.rawCh0 if !ok { return 0, nil, c.Error() } - cmd = binary.LittleEndian.Uint16(buf[:2]) - data = buf[4:] - return + return binary.LittleEndian.Uint32(buf), buf[4:], nil } -func (c *Conn) WriteCommand(cmd uint16, data []byte) error { +func (c *Conn) WriteCommand(cmd uint32, data []byte) error { c.cmdMu.Lock() defer c.cmdMu.Unlock() - req := marshalCmd(0, c.seqCh0, uint32(cmd), data) + req := marshalCmd(0, c.seqCh0, cmd, data) c.seqCh0++ if c.isTCP { @@ -268,18 +270,18 @@ func (c *Conn) WriteCommand(cmd uint16, data []byte) error { } } -func (c *Conn) ReadPacket() ([]byte, error) { +func (c *Conn) ReadPacket() (hdr, payload []byte, err error) { data, ok := <-c.rawCh2 if !ok { - return nil, c.Error() + return nil, nil, c.Error() } - return data, nil + return data[:32], data[32:], nil } -func (c *Conn) WritePacket(data []byte) error { +func (c *Conn) WritePacket(hdr, payload []byte) error { const offset = 12 - n := uint32(len(data)) + n := 32 + uint32(len(payload)) req := make([]byte, n+offset) req[0] = magic req[1] = msgDrw @@ -290,7 +292,8 @@ func (c *Conn) WritePacket(data []byte) error { binary.BigEndian.PutUint16(req[6:], c.seqCh3) c.seqCh3++ binary.BigEndian.PutUint32(req[8:], n) - copy(req[offset:], data) + copy(req[offset:], hdr) + copy(req[offset+32:], payload) _, err := c.conn.Write(req) return err diff --git a/pkg/xiaomi/miss/producer.go b/pkg/xiaomi/miss/producer.go new file mode 100644 index 00000000..eeaa4969 --- /dev/null +++ b/pkg/xiaomi/miss/producer.go @@ -0,0 +1,204 @@ +package miss + +import ( + "fmt" + "net/url" + "time" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/h264" + "github.com/AlexxIT/go2rtc/pkg/h264/annexb" + "github.com/AlexxIT/go2rtc/pkg/h265" + "github.com/pion/rtp" +) + +type Producer struct { + core.Connection + client *Client +} + +func Dial(rawURL string) (core.Producer, error) { + client, err := NewClient(rawURL) + if err != nil { + return nil, err + } + + u, _ := url.Parse(rawURL) + query := u.Query() + + err = client.StartMedia(query.Get("channel"), query.Get("subtype"), query.Get("audio")) + if err != nil { + _ = client.Close() + return nil, err + } + + medias, err := probe(client, query.Get("audio") != "0") + if err != nil { + _ = client.Close() + return nil, err + } + + return &Producer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "xiaomi/miss", + Protocol: client.Protocol(), + RemoteAddr: client.RemoteAddr().String(), + UserAgent: client.Version(), + Medias: medias, + Transport: client, + }, + client: client, + }, nil +} + +func probe(client *Client, audio bool) ([]*core.Media, error) { + _ = client.SetDeadline(time.Now().Add(15 * time.Second)) + + var vcodec, acodec *core.Codec + + for { + pkt, err := client.ReadPacket() + if err != nil { + if vcodec != nil { + err = fmt.Errorf("no audio") + } else if acodec != nil { + err = fmt.Errorf("no video") + } + return nil, fmt.Errorf("xiaomi: probe: %w", err) + } + + switch pkt.CodecID { + case codecH264: + if vcodec == nil { + buf := annexb.EncodeToAVCC(pkt.Payload) + if h264.NALUType(buf) == h264.NALUTypeSPS { + vcodec = h264.AVCCToCodec(buf) + } + } + case codecH265: + if vcodec == nil { + buf := annexb.EncodeToAVCC(pkt.Payload) + if h265.NALUType(buf) == h265.NALUTypeVPS { + vcodec = h265.AVCCToCodec(buf) + } + } + case codecPCMA: + if acodec == nil { + acodec = &core.Codec{Name: core.CodecPCMA, ClockRate: 8000} + } + case codecOPUS: + if acodec == nil { + acodec = &core.Codec{Name: core.CodecOpus, ClockRate: 48000, Channels: 2} + } + } + + if vcodec != nil && (acodec != nil || !audio) { + break + } + } + + _ = client.SetDeadline(time.Time{}) + + medias := []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{vcodec}, + }, + } + + if acodec != nil { + medias = append(medias, &core.Media{ + Kind: core.KindAudio, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{acodec}, + }) + + medias = append(medias, &core.Media{ + Kind: core.KindAudio, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{acodec.Clone()}, + }) + } + + return medias, nil +} + +const timestamp40ms = 48000 * 0.040 + +func (p *Producer) Start() error { + var audioTS uint32 + + for { + _ = p.client.SetDeadline(time.Now().Add(10 * time.Second)) + pkt, err := p.client.ReadPacket() + if err != nil { + return err + } + + p.Recv += len(pkt.Payload) + + // TODO: rewrite this + var name string + var pkt2 *core.Packet + + switch pkt.CodecID { + case codecH264, codecH265: + pkt2 = &core.Packet{ + Header: rtp.Header{ + SequenceNumber: uint16(pkt.Sequence), + Timestamp: TimeToRTP(pkt.Timestamp, 90000), + }, + Payload: annexb.EncodeToAVCC(pkt.Payload), + } + if pkt.CodecID == codecH264 { + name = core.CodecH264 + } else { + name = core.CodecH265 + } + case codecPCMA: + name = core.CodecPCMA + pkt2 = &core.Packet{ + Header: rtp.Header{ + Version: 2, + Marker: true, + SequenceNumber: uint16(pkt.Sequence), + Timestamp: audioTS, + }, + Payload: pkt.Payload, + } + audioTS += uint32(len(pkt.Payload)) + case codecOPUS: + name = core.CodecOpus + pkt2 = &core.Packet{ + Header: rtp.Header{ + Version: 2, + Marker: true, + SequenceNumber: uint16(pkt.Sequence), + Timestamp: audioTS, + }, + Payload: pkt.Payload, + } + // known cameras sends packets with 40ms long + audioTS += timestamp40ms + } + + for _, recv := range p.Receivers { + if recv.Codec.Name == name { + recv.WriteRTP(pkt2) + break + } + } + } +} + +func (p *Producer) Stop() error { + _ = p.client.StopMedia() + return p.Connection.Stop() +} + +// TimeToRTP convert time in milliseconds to RTP time +func TimeToRTP(timeMS, clockRate uint64) uint32 { + return uint32(timeMS * clockRate / 1000) +} diff --git a/pkg/xiaomi/producer.go b/pkg/xiaomi/producer.go index 805a3f86..d0290f2f 100644 --- a/pkg/xiaomi/producer.go +++ b/pkg/xiaomi/producer.go @@ -1,229 +1,23 @@ package xiaomi import ( - "fmt" - "net/url" - "time" + "strings" "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/AlexxIT/go2rtc/pkg/h264" - "github.com/AlexxIT/go2rtc/pkg/h264/annexb" - "github.com/AlexxIT/go2rtc/pkg/h265" + "github.com/AlexxIT/go2rtc/pkg/xiaomi/legacy" "github.com/AlexxIT/go2rtc/pkg/xiaomi/miss" - "github.com/pion/rtp" ) -type Producer struct { - core.Connection - client *miss.Client - model string -} - func Dial(rawURL string) (core.Producer, error) { - client, err := miss.Dial(rawURL) - if err != nil { - return nil, err + // Format: xiaomi/miss + if strings.Contains(rawURL, "vendor") { + return miss.Dial(rawURL) } - u, _ := url.Parse(rawURL) - query := u.Query() - - // 0 - main, 1 - second - channel := core.ParseByte(query.Get("channel")) - - // 0 - auto, 1 - worst, 3 or 5 - best - var quality byte - switch s := query.Get("subtype"); s { - case "", "hd": - quality = 3 - case "sd": - quality = 1 - case "auto": - quality = 0 - default: - quality = core.ParseByte(s) - } - - // 0 - disabled, 1 - enabled, 2 - enabled (another API) - var audio byte - switch s := query.Get("audio"); s { - case "", "1": - audio = 1 - default: - audio = core.ParseByte(s) - } - - medias, err := probe(client, channel, quality, audio) - if err != nil { - _ = client.Close() - return nil, err - } - - return &Producer{ - Connection: core.Connection{ - ID: core.NewID(), - FormatName: "xiaomi", - Protocol: client.Protocol(), - RemoteAddr: client.RemoteAddr().String(), - Source: rawURL, - Medias: medias, - Transport: client, - }, - client: client, - model: query.Get("model"), - }, nil + // Format: xiaomi/legacy + return legacy.Dial(rawURL) } -func probe(client *miss.Client, channel, quality, audio uint8) ([]*core.Media, error) { - _ = client.SetDeadline(time.Now().Add(10 * time.Second)) - - if err := client.VideoStart(channel, quality, audio&1); err != nil { - return nil, err - } - - if audio > 1 { - _ = client.AudioStart() - } - - var vcodec, acodec *core.Codec - - for { - pkt, err := client.ReadPacket() - if err != nil { - return nil, fmt.Errorf("xiaomi: probe: %w", err) - } - - switch pkt.CodecID { - case miss.CodecH264: - if vcodec == nil { - buf := annexb.EncodeToAVCC(pkt.Payload) - if h264.NALUType(buf) == h264.NALUTypeSPS { - vcodec = h264.AVCCToCodec(buf) - } - } - case miss.CodecH265: - if vcodec == nil { - buf := annexb.EncodeToAVCC(pkt.Payload) - if h265.NALUType(buf) == h265.NALUTypeVPS { - vcodec = h265.AVCCToCodec(buf) - } - } - case miss.CodecPCMA: - if acodec == nil { - acodec = &core.Codec{Name: core.CodecPCMA, ClockRate: 8000} - } - case miss.CodecOPUS: - if acodec == nil { - acodec = &core.Codec{Name: core.CodecOpus, ClockRate: 48000, Channels: 2} - } - } - - if vcodec != nil && (acodec != nil || audio == 0) { - break - } - } - - _ = client.SetDeadline(time.Time{}) - - medias := []*core.Media{ - { - Kind: core.KindVideo, - Direction: core.DirectionRecvonly, - Codecs: []*core.Codec{vcodec}, - }, - } - - if acodec != nil { - medias = append(medias, &core.Media{ - Kind: core.KindAudio, - Direction: core.DirectionRecvonly, - Codecs: []*core.Codec{acodec}, - }) - - medias = append(medias, &core.Media{ - Kind: core.KindAudio, - Direction: core.DirectionSendonly, - Codecs: []*core.Codec{acodec.Clone()}, - }) - } - - return medias, nil -} - -const timestamp40ms = 48000 * 0.040 - -func (p *Producer) Start() error { - var audioTS uint32 - - for { - _ = p.client.SetDeadline(time.Now().Add(10 * time.Second)) - pkt, err := p.client.ReadPacket() - if err != nil { - return err - } - - p.Recv += len(pkt.Payload) - - // TODO: rewrite this - var name string - var pkt2 *core.Packet - - switch pkt.CodecID { - case miss.CodecH264: - name = core.CodecH264 - pkt2 = &core.Packet{ - Header: rtp.Header{ - SequenceNumber: uint16(pkt.Sequence), - Timestamp: TimeToRTP(pkt.Timestamp, 90000), - }, - Payload: annexb.EncodeToAVCC(pkt.Payload), - } - case miss.CodecH265: - name = core.CodecH265 - pkt2 = &core.Packet{ - Header: rtp.Header{ - SequenceNumber: uint16(pkt.Sequence), - Timestamp: TimeToRTP(pkt.Timestamp, 90000), - }, - Payload: annexb.EncodeToAVCC(pkt.Payload), - } - case miss.CodecPCMA: - name = core.CodecPCMA - pkt2 = &core.Packet{ - Header: rtp.Header{ - Version: 2, - Marker: true, - SequenceNumber: uint16(pkt.Sequence), - Timestamp: audioTS, - }, - Payload: pkt.Payload, - } - audioTS += uint32(len(pkt.Payload)) - case miss.CodecOPUS: - name = core.CodecOpus - pkt2 = &core.Packet{ - Header: rtp.Header{ - Version: 2, - Marker: true, - SequenceNumber: uint16(pkt.Sequence), - Timestamp: audioTS, - }, - Payload: pkt.Payload, - } - // known cameras sends packets with 40ms long - audioTS += timestamp40ms - } - - for _, recv := range p.Receivers { - if recv.Codec.Name == name { - recv.WriteRTP(pkt2) - break - } - } - } -} - -// TimeToRTP convert time in milliseconds to RTP time -func TimeToRTP(timeMS, clockRate uint64) uint32 { - return uint32(timeMS * clockRate / 1000) +func IsLegacy(model string) bool { + return legacy.Supported(model) } diff --git a/pkg/xiaomi/tutk/README.md b/pkg/xiaomi/tutk/README.md deleted file mode 100644 index 95773594..00000000 --- a/pkg/xiaomi/tutk/README.md +++ /dev/null @@ -1,63 +0,0 @@ -# TUTK - -The most terrible protocol I have ever had to work with. - -## Messages - -Ping from camera (24b). The shortest message. - -``` -off sample -0 0402 tutk magic -2 190a tutk version (120a, 190a...) -4 0800 msg size = len(b)-16 = 24-16 -6 0000 channel seq (always 0 for ping) -8 2804 msg type (2804 - ping from camera, 0804 - usual msg from camera) -10 1200 direction (12 - from camera, 21 - from client) -12 00000000 fixed -16 7ecc93c4 random -20 56c2561f random -``` - -Usual msg from camera (52b + msg data). - -``` -off sample -12 e6e8 same bytes b[20:22] -14 0000 channel (0, 1, 5) -16 0c00 fixed -18 0000 fixed -20 e6e839da random session id -24 66b0dc14 random session id -28 0070 command -30 0b00 version -32 0100 command seq -34 0000 ??? -36 00000000 ??? -40 00000000 ??? -44 e300 msg data size -46 0000 ??? -48 8f15a02f random msg id -52 ... msg data -``` - -Message with media from camera. - -``` -off sample -28 0c00 command -30 0b00 version -32 7700 command seq -34 0000 ??? data only for last message per pack (14/14) -36 0200 pack seq, don't know how packs used -38 0914 09/14 - message seq/messages per packs -40 01000000 fixed -42 0500 command 2 -44 3200 command 2 seq -46 4f00 chunks count per this frame -48 1b00 chunk seq, starts from 0 (wrong for last chunk) -50 0004 frame data size -52 c8f6 random msg id -54 01000000 previous frame seq, starts from 0 -58 02000000 current frame seq, starts from 1 -``` diff --git a/pkg/xiaomi/tutk/conn.go b/pkg/xiaomi/tutk/conn.go deleted file mode 100644 index 1b0ea56e..00000000 --- a/pkg/xiaomi/tutk/conn.go +++ /dev/null @@ -1,262 +0,0 @@ -package tutk - -import ( - "bytes" - "crypto/rand" - "encoding/binary" - "fmt" - "io" - "net" - "sync" - "sync/atomic" - "time" -) - -func Dial(host, uid, model string) (*Conn, error) { - conn, err := net.ListenUDP("udp", nil) - if err != nil { - return nil, err - } - - addr, err := net.ResolveUDPAddr("udp", host) - if err != nil { - addr = &net.UDPAddr{IP: net.ParseIP(host), Port: 32761} - } - - c := &Conn{conn: conn, addr: addr, sid: genSID()} - - if err = c.handshake([]byte(uid)); err != nil { - _ = c.Close() - return nil, err - } - - switch model { - case "isa.camera.df3": - c.msgCtrl = c.oldMsgCtrl - c.handleCh0 = c.oldHandlerCh0() - default: - c.msgCtrl = c.newMsgCtrl - c.handleCh0 = c.newHandlerCh0() - } - - c.rawCmd = make(chan []byte, 10) - c.rawPkt = make(chan []byte, 100) - - go c.worker() - - return c, nil -} - -type Conn struct { - conn *net.UDPConn - addr *net.UDPAddr - sid []byte - - err error - rawCmd chan []byte - rawPkt chan []byte - - cmdMu sync.Mutex - cmdAck func() - - seqSendCh0 uint16 - seqSendCh1 uint16 - - seqSendCmd1 uint16 - seqSendCmd2 uint16 - seqSendCnt uint16 - seqSendAud uint16 - - seqRecvPkt0 uint16 - seqRecvPkt1 uint16 - seqRecvCmd2 uint16 - - msgCtrl func(ctrlType uint16, ctrlData []byte) []byte - handleCh0 func(cmd []byte) int8 -} - -func (c *Conn) handshake(uid []byte) (err error) { - _ = c.SetDeadline(time.Now().Add(5 * time.Second)) - - if _, err = c.WriteAndWait( - c.msgConnectByUID(uid, 1), - func(_, res []byte) bool { - return bytes.Index(res, uid) == 16 // 02061200 - }, - ); err != nil { - return err - } - - if err = c.Write(c.msgConnectByUID(uid, 2)); err != nil { - return err - } - - if _, err = c.WriteAndWait( - c.msgAvClientStart(), - func(req, res []byte) bool { - return bytes.Index(res, req[48:52]) == 48 - }, - ); err != nil { - return err - } - - _ = c.SetDeadline(time.Time{}) - - return nil -} - -func (c *Conn) worker() { - defer func() { - close(c.rawCmd) - close(c.rawPkt) - }() - - buf := make([]byte, 1200) - - for { - n, _, err := c.ReadFromUDP(buf) - if err != nil { - c.err = fmt.Errorf("%s: %w", "tutk", err) - return - } - - if c.handleMsg(buf[:n]) <= 0 { - if c.err != nil { - return - } - fmt.Printf("tutk: unknown msg: %x\n", buf[:n]) - } - } -} - -func (c *Conn) Write(buf []byte) error { - //log.Printf("-> %x", buf) - _, err := c.conn.WriteToUDP(TransCodePartial(nil, buf), c.addr) - return err -} - -func (c *Conn) ReadFromUDP(buf []byte) (n int, addr *net.UDPAddr, err error) { - for { - if n, addr, err = c.conn.ReadFromUDP(buf); err != nil { - return 0, nil, err - } - - if string(addr.IP) != string(c.addr.IP) || n < 16 { - continue // skip messages from another IP - } - - ReverseTransCodePartial(buf, buf[:n]) - //log.Printf("<- %x", buf[:n]) - return n, addr, nil - } -} - -func (c *Conn) WriteAndWait(req []byte, ok func(req, res []byte) bool) ([]byte, error) { - var t *time.Timer - t = time.AfterFunc(1, func() { - if err := c.Write(req); err == nil && t != nil { - t.Reset(time.Second) - } - }) - defer t.Stop() - - buf := make([]byte, 1200) - - for { - n, addr, err := c.ReadFromUDP(buf) - if err != nil { - return nil, err - } - - if ok(req, buf[:n]) { - c.addr.Port = addr.Port - return buf[:n], nil - } - } -} - -func (c *Conn) Protocol() string { - return "tutk+udp" -} - -func (c *Conn) RemoteAddr() net.Addr { - return c.addr -} - -func (c *Conn) SetDeadline(t time.Time) error { - return c.conn.SetDeadline(t) -} - -func (c *Conn) Close() error { - return c.conn.Close() -} - -func (c *Conn) Error() error { - if c.err != nil { - return c.err - } - return io.EOF -} - -func (c *Conn) ReadCommand() (cmd uint16, data []byte, err error) { - buf, ok := <-c.rawCmd - if !ok { - return 0, nil, c.Error() - } - cmd = binary.LittleEndian.Uint16(buf[:2]) - data = buf[4:] - return -} - -// WriteCommand will send a command every second five times -func (c *Conn) WriteCommand(cmd uint16, data []byte) error { - c.cmdMu.Lock() - defer c.cmdMu.Unlock() - - var repeat atomic.Int32 - repeat.Store(5) - - timeout := time.NewTicker(time.Second) - defer timeout.Stop() - - c.cmdAck = func() { - repeat.Store(0) - timeout.Reset(1) - } - - msg := c.msgCtrl(cmd, data) - - for { - if err := c.WriteCh0(msg); err != nil { - return err - } - <-timeout.C - r := repeat.Add(-1) - if r < 0 { - return nil - } - if r == 0 { - return fmt.Errorf("%s: can't send command %d", "tutk", cmd) - } - } -} - -func (c *Conn) ReadPacket() ([]byte, error) { - buf, ok := <-c.rawPkt - if !ok { - return nil, c.Error() - } - return buf, nil -} - -func (c *Conn) WritePacket(data []byte) error { - return c.WriteCh1(c.oldMsgAud(data)) -} - -func genSID() []byte { - b := make([]byte, 16) - _, _ = rand.Read(b[8:]) - copy(b, b[8:10]) - b[4] = 0x0c - return b -} diff --git a/pkg/xiaomi/tutk/proto.go b/pkg/xiaomi/tutk/proto.go deleted file mode 100644 index a440900a..00000000 --- a/pkg/xiaomi/tutk/proto.go +++ /dev/null @@ -1,251 +0,0 @@ -package tutk - -import ( - "encoding/binary" - "time" -) - -func (c *Conn) WriteCh0(msg []byte) error { - binary.LittleEndian.PutUint16(msg[6:], c.seqSendCh0) - c.seqSendCh0++ - return c.Write(msg) -} - -func (c *Conn) WriteCh1(msg []byte) error { - binary.LittleEndian.PutUint16(msg[6:], c.seqSendCh1) - c.seqSendCh1++ - msg[14] = 1 // channel - return c.Write(msg) -} - -func (c *Conn) msgConnectByUID(uid []byte, i byte) []byte { - const size = 68 // or 52 or 68 or 88 - b := make([]byte, size) - copy(b, "\x04\x02\x19\x02") - b[4] = size - 16 - copy(b[8:], "\x01\x06\x21\x00") - copy(b[16:], uid) - copy(b[52:], "\x00\x03\x01\x02") // or 07000303 or 01010204 - copy(b[56:], c.sid[8:]) - b[64] = i // 1 or 2 - return b -} - -func (c *Conn) msgAvClientStart() []byte { - const size = 566 + 32 - msg := c.msg(size) - - cmd := msg[msgHhrSize:] - copy(cmd, "\x00\x00\x0b\x00") - binary.LittleEndian.PutUint16(cmd[16:], size-52) - //cmd[18] = 1 // ??? - binary.LittleEndian.PutUint32(cmd[20:], uint32(time.Now().UnixMilli())) - - // important values for some cameras (not for df3) - data := cmd[cmdHdrSize:] - copy(data, "Miss") - copy(data[257:], "client") - - // 0100000004000000fb071f000000000000000000000003000000000001000000 - cfg := msg[566:] - cfg[0] = 0 // 0 - simple proto, 1 - complex proto with "0Cxx" commands - cfg[4] = 4 - copy(cfg[8:], "\xfb\x07\x1f\x00") - cfg[22] = 3 - cfg[28] = 1 - return msg -} - -func (c *Conn) msg(size uint16) []byte { - b := make([]byte, size) - copy(b, "\x04\x02\x19\x0a") - binary.LittleEndian.PutUint16(b[4:], size-16) - copy(b[8:], "\x07\x04\x21\x00") - copy(b[12:], c.sid) - return b -} - -const ( - msgPing = iota + 1 - msgClientStart00 - msgClientStart20 - msgCommand - msgCounters - msgMediaChunk - msgMediaFrame - msgMediaLost - msgCh5 - msgUnknown0007 - msgUnknown0008 - msgUnknown0010 - msgUnknown0013 - msgUnknown0a08 - msgDafang0012 - msgDafang0071 -) - -// handleMsg will return parsed msg type or zero -func (c *Conn) handleMsg(msg []byte) int8 { - //log.Printf("<- %x", msg) - // off sample - // 0 0402 tutk magic - // 2 120a tutk version (120a, 190a...) - // 4 0800 msg size = len(b)-16 - // 6 0000 channel seq - // 8 28041200 msg type - // 14 0100 channel (not all msg) - // 28 0700 msg data (not all msg) - switch msg[8] { - case 0x28: - _ = c.Write(msgAckPing(msg)) - return msgPing - case 0x08: - switch ch := msg[14]; ch { - case 0: - return c.handleCh0(msg[28:]) - case 1: - return c.handleCh1(msg[28:]) - case 5: - return c.handleCh5(msg) - } - } - return 0 -} - -func (c *Conn) handleCh1(cmd []byte) int8 { - // Channel 1 used for two-way audio. It's important: - // - answer on 0000 command with exact config response (can't set simple proto) - // - send 0012 command at start - // - respond on every 0008 command for smooth playback - switch cid := string(cmd[:2]); cid { - case "\x00\x00": // client start - _ = c.WriteCh1(c.msgAck0000(cmd)) - _ = c.WriteCh1(c.msg0012()) - return msgClientStart00 - case "\x00\x07": // time sync without data - _ = c.WriteCh1(c.msgAck0007(cmd)) - return msgUnknown0007 - case "\x00\x08": // time sync with data - _ = c.WriteCh1(c.msgAck0008(cmd)) - return msgUnknown0008 - case "\x00\x13": // ack for 0012 - return msgUnknown0013 - case "\x00\x20": // client start2 - //_ = c.WriteCh1(c.msgAck0020(cmd)) - return msgClientStart20 - case "\x09\x00": // counters sync - return msgCounters - case "\x0a\x08": // unknown - _ = c.WriteCh1(c.msgAck0A08(cmd)) - return msgUnknown0a08 - } - return 0 -} - -func (c *Conn) handleCh5(msg []byte) int8 { - if len(msg) != 48 { - return 0 - } - - // <- [48] 0402190a 20000400 07042100 7ecc05000c0000007ecc93c456c2561f 5a97c2f101050000000000000000000000010000 - // -> [48] 0402190a 20000400 08041200 7ecc05000c0000007ecc93c456c2561f 5a97c2f141050000000000000000000000010000 - copy(msg[8:], "\x07\x04\x21\x00") - msg[32] = 0x41 - _ = c.Write(msg) - return msgCh5 -} - -const msgHhrSize = 28 -const cmdHdrSize = 24 - -func (c *Conn) msgAck0000(msg28 []byte) []byte { - // <- 000008000000000000000000000000001a0200004f47c714 ... 00000000000000000100000004000000fb071f00000000000000000000000300 - // -> 00140b00000000000000000000000000200000004f47c714 00000000000000000100000004000000fb071f00000000000000000000000300 - const cmdDataSize = 32 - msg := c.msg(msgHhrSize + cmdHdrSize + cmdDataSize) - - cmd := msg[msgHhrSize:] - copy(cmd, "\x00\x14\x0b\x00") - cmd[16] = cmdDataSize - copy(cmd[20:], msg28[20:24]) // request id (random) - - // Important to answer with same data. - data := cmd[cmdHdrSize:] - copy(data, msg28[len(msg28)-32:]) - return msg -} - -//func (c *Conn) msgAck0020(msg28 []byte) []byte { -// const cmdDataSize = 36 -// -// msg := c.msg(msgHhrSize + cmdHdrSize + cmdDataSize) -// -// cmd := msg[msgHhrSize:] -// copy(cmd, "\x00\x14\x0b\x00") -// cmd[16] = cmdDataSize -// copy(cmd[20:], msg28[20:24]) // request id (random) -// -// data := cmd[cmdHdrSize:] -// data[5] = 1 -// data[7] = 1 -// data[8] = 1 -// data[12] = 4 -// copy(data[16:], "\xfb\x07\x1f\x00") -// data[30] = 3 -// data[32] = 1 -// return msg -//} - -func (c *Conn) msg0012() []byte { - // -> 00120b000000000000000000000000000c00000000000000020000000100000001000000 - const dataSize = 12 - msg := c.msg(msgHhrSize + cmdHdrSize + dataSize) - cmd := msg[msgHhrSize:] - - copy(cmd, "\x00\x12\x0b\x00") - cmd[16] = dataSize - data := cmd[cmdHdrSize:] - - data[0] = 2 - data[4] = 1 - data[9] = 1 - return msg -} - -func (c *Conn) msgAck0007(msg28 []byte) []byte { - // <- 000708000000000000000000000000000c00000001000000000000001c551f7a00000000 - // -> 010a0b00000000000000000000000000000000000100000000000000 - msg := c.msg(msgHhrSize + 28) - cmd := msg[msgHhrSize:] - copy(cmd, "\x01\x0a\x0b\x00") - cmd[20] = 1 - return msg -} - -func (c *Conn) msgAck0008(msg28 []byte) []byte { - // <- 000808000000000000000000000000000000f9f0010000000200000050f31f7a - // -> 01090b0000000000000000000000000000000000010000000200000050f31f7a - msg := c.msg(msgHhrSize + 28) - cmd := msg[msgHhrSize:] - copy(cmd, "\x01\x09\x0b\x00") - copy(cmd[20:], msg28[20:]) - return msg -} - -func (c *Conn) msgAck0A08(msg28 []byte) []byte { - // <- 0a080b005b0000000b51590002000000 - // -> 0b000b00000001000b5103000300000000000000 - msg := c.msg(msgHhrSize + 20) - cmd := msg[msgHhrSize:] - copy(cmd, "\x0b\x00\x0b\x00") - copy(cmd[8:], msg28[8:10]) - return msg -} - -func msgAckPing(req []byte) []byte { - // <- [24] 0402120a 08000000 28041200 000000005b0d4202070aa8c0 - // -> [24] 04021a0a 08000000 27042100 000000005b0d4202070aa8c0 - req[8] = 0x27 - req[10] = 0x21 - return req -} diff --git a/pkg/xiaomi/tutk/proto_new.go b/pkg/xiaomi/tutk/proto_new.go deleted file mode 100644 index e7a08080..00000000 --- a/pkg/xiaomi/tutk/proto_new.go +++ /dev/null @@ -1,191 +0,0 @@ -package tutk - -import ( - "encoding/binary" - "fmt" - "time" -) - -func (c *Conn) newMsgCtrl(ctrlType uint16, ctrlData []byte) []byte { - size := msgHhrSize + 28 + 4 + uint16(len(ctrlData)) - msg := c.msg(size) - - // 0 0070 command - // 2 0b00 version - // 4 1000 seq - // 6 0076 ??? - cmd := msg[msgHhrSize:] - copy(cmd, "\x00\x70\x0b\x00") - binary.LittleEndian.PutUint16(cmd[4:], c.seqSendCmd1) - c.seqSendCmd1++ - - // 8 0070 command (second time) - // 10 0300 seq - // 12 0100 chunks count - // 14 0000 chunk seq (starts from 0) - // 16 5500 size - // 18 0000 random msg id (always 0) - // 20 03000000 seq (second time) - // 24 00000000 - // 28 01010000 ctrlType - cmd[9] = 0x70 - cmd[12] = 1 - binary.LittleEndian.PutUint16(cmd[16:], size-52) - - binary.LittleEndian.PutUint16(cmd[10:], c.seqSendCmd2) - binary.LittleEndian.PutUint16(cmd[20:], c.seqSendCmd2) - c.seqSendCmd2++ - - data := cmd[28:] - binary.LittleEndian.PutUint16(data, ctrlType) - copy(data[4:], ctrlData) - return msg -} - -func (c *Conn) newHandlerCh0() func(msg []byte) int8 { - var waitData []byte - var waitSeq uint16 - - return func(cmd []byte) int8 { - switch cmd[0] { - case 0x07, 0x05: - flag := cmd[1] - - var cmd2 []byte - if flag&0b1000 == 0 { - // off sample - // 0 0700 command - // 2 0b00 version - // 4 2700 seq - // 6 0000 ??? - // 8 0700 command (second time) - // 10 1400 seq - // 12 1300 chunks count per this frame - // 14 1100 chunk seq, starts from 0 (0x20 for last chunk) - // 16 0004 frame data size - // 18 0000 random msg id (always 0) - // 20 02000000 previous frame seq, starts from 0 - // 24 03000000 current frame seq, starts from 1 - cmd2 = cmd[8:] - } else { - // off sample - // 0 070d0b00 - // 4 30000000 - // 8 5c965500 ??? - // 12 ffff0000 ??? - // 16 0701 fixed command - // 18 190001002000a802000006000000070000000 - cmd2 = cmd[16:] - } - - seq := binary.LittleEndian.Uint16(cmd2[2:]) - - // Check if this is first chunk for frame. - // Handle protocol bug "0x20 chunk seq for last chunk" and sometimes - // "0x20 chunk seq for first chunk if only one chunk". - if binary.LittleEndian.Uint16(cmd2[6:]) == 0 || binary.LittleEndian.Uint16(cmd2[4:]) == 1 { - waitData = waitData[:0] - waitSeq = seq - } else if seq != waitSeq { - return msgMediaLost - } - - if flag&0b0001 == 0 { - waitData = append(waitData, cmd2[20:]...) - waitSeq++ - return msgMediaChunk - } - - c.seqRecvPkt1 = seq - _ = c.WriteCh0(c.msgAckCounters()) - - data := cmd2[20:] - n := len(data) - 32 - waitData = append(waitData, data[:n]...) - - packetData := make([]byte, 32+len(waitData)) - copy(packetData, data[n:]) - copy(packetData[32:], waitData) - - select { - case c.rawPkt <- packetData: - default: - c.err = fmt.Errorf("%s: media queue is full", "tutk") - return -1 - } - return msgMediaFrame - - case 0x00: - _ = c.WriteCh0(c.msgAckCounters()) - c.seqRecvCmd2 = binary.LittleEndian.Uint16(cmd[2:]) - - switch cmd[1] { - case 0x10: - return msgUnknown0010 // unknown - case 0x70: - select { - case c.rawCmd <- cmd[28:]: - default: - } - return msgCommand // cmd from camera - } - - case 0x09: - // off sample - // 0 09000b00 cmd1 - // 4 0d000000 seqCmd1 - // 12 0000 seqRecvCmd2 - seq := binary.LittleEndian.Uint16(cmd[12:]) - if c.seqSendCmd1 > seq { - if c.cmdAck != nil { - c.cmdAck() - } - } - return msgCounters - - case 0x0a: - // seq sample - // 0 0a080b00 - // 4 03000000 - // 8 e2043200 - // 12 01000000 - _ = c.WriteCh0(c.msgAck0A08(cmd)) - return msgUnknown0a08 - } - - return 0 - } -} - -func (c *Conn) msgAckCounters() []byte { - msg := c.msg(msgHhrSize + cmdHdrSize) - - // off sample - // 0 09000b00 cmd1 - // 4 2700 seqCmd1 - // 6 0000 - // 8 1300 seqRecvPkt0 - // 10 2600 seqRecvPkt1 - // 12 0400 seqRecvCmd2 - // 14 00000000 - // 18 1400 seqSendCnt - // 20 d91a random - // 22 0000 - cmd := msg[msgHhrSize:] - copy(cmd, "\x09\x00\x0b\x00") - - binary.LittleEndian.PutUint16(cmd[4:], c.seqSendCmd1) - c.seqSendCmd1++ - - // seqRecvPkt0 stores previous value of seqRecvPkt1 - // don't understand why this needs - binary.LittleEndian.PutUint16(cmd[8:], c.seqRecvPkt0) - c.seqRecvPkt0 = c.seqRecvPkt1 - binary.LittleEndian.PutUint16(cmd[10:], c.seqRecvPkt1) - binary.LittleEndian.PutUint16(cmd[12:], c.seqRecvCmd2) - - binary.LittleEndian.PutUint16(cmd[18:], c.seqSendCnt) - c.seqSendCnt++ - binary.LittleEndian.PutUint16(cmd[20:], uint16(time.Now().UnixMilli())) - return msg -} diff --git a/pkg/xiaomi/tutk/proto_old.go b/pkg/xiaomi/tutk/proto_old.go deleted file mode 100644 index 875c67a9..00000000 --- a/pkg/xiaomi/tutk/proto_old.go +++ /dev/null @@ -1,192 +0,0 @@ -package tutk - -import ( - "encoding/binary" - "fmt" - "time" -) - -func (c *Conn) oldMsgCtrl(ctrlType uint16, ctrlData []byte) []byte { - dataSize := 4 + uint16(len(ctrlData)) - msg := c.msg(msgHhrSize + cmdHdrSize + dataSize) - - cmd := msg[msgHhrSize:] - copy(cmd, "\x00\x70\x0b\x00") - - binary.LittleEndian.PutUint16(cmd[4:], c.seqSendCmd1) - c.seqSendCmd1++ - - binary.LittleEndian.PutUint16(cmd[16:], dataSize) - //binary.LittleEndian.PutUint32(cmd[20:], uint32(time.Now().UnixMilli())) - - data := cmd[cmdHdrSize:] - binary.LittleEndian.PutUint16(data, ctrlType) - copy(data[4:], ctrlData) - return msg -} - -const pktHdrSize = 32 - -func (c *Conn) oldMsgAud(pkt []byte) []byte { - // -> 01030b001d0000008802000000002800b0020bf501000000 ... 4f4455412000000088020000030400001d000000000000000bf51f7a9b0100000000000000000000 - hdr := pkt[:pktHdrSize] - payload := pkt[pktHdrSize:] - - n := uint16(len(payload)) - dataSize := n + 8 + 32 - msg := c.msg(msgHhrSize + cmdHdrSize + dataSize) - - // 0 01030b00 command + version - // 4 1d000000 seq - // 8 8802 media size (648) - // 10 00000000 - // 14 2800 tail (pkt header) size? - // 16 b002 size (648 + 8 + 32) - // 18 0bf5 random msg id (unixms) - // 20 01000000 fixed - cmd := msg[msgHhrSize:] - copy(cmd, "\x01\x03\x0b\x00") - binary.LittleEndian.PutUint16(cmd[4:], c.seqSendAud) - c.seqSendAud++ - binary.LittleEndian.PutUint16(cmd[8:], n) - cmd[14] = 0x28 // important! - binary.LittleEndian.PutUint16(cmd[16:], dataSize) - binary.LittleEndian.PutUint16(cmd[18:], uint16(time.Now().UnixMilli())) - cmd[20] = 1 - - data := cmd[cmdHdrSize:] - copy(data, payload) - copy(data[n:], "ODUA\x20\x00\x00\x00") - copy(data[n+8:], hdr) - - return msg -} - -func (c *Conn) oldHandlerCh0() func([]byte) int8 { - var waitSeq uint16 - var waitSize uint32 - var waitData []byte - - return func(cmd []byte) int8 { - // 0 01030800 command + version - // 4 00000000 fixed - // 8 ac880100 total size - // 12 6200 chunk seq - // 14 2000 tail (pkt header) size? - // 16 cc00 size - // 18 0000 - // 20 01000000 fixed - - switch cmd[0] { - case 0x01: - var packetData []byte - - switch cmd[1] { - case 0x03: - seq := binary.LittleEndian.Uint16(cmd[12:]) - if seq != waitSeq { - waitSeq = 0 - return msgMediaLost - } - if seq == 0 { - waitData = waitData[:0] - waitSize = binary.LittleEndian.Uint32(cmd[8:]) + 32 - } - - waitData = append(waitData, cmd[24:]...) - if n := uint32(len(waitData)); n < waitSize { - waitSeq++ - return msgMediaChunk - } else if n > waitSize { - waitSeq = 0 - return msgMediaLost - } - - waitSeq = 0 - - // create a buffer for the header and collected data - packetData = make([]byte, waitSize) - // there's a header at the end - let's move it to the beginning - copy(packetData, waitData[waitSize-32:]) - copy(packetData[32:], waitData) - - case 0x04: - // This is audio from miss audio start command. MiHome not using miss commands. - waitSize2 := binary.LittleEndian.Uint32(cmd[8:]) - waitData2 := cmd[24:] - - if uint32(len(waitData2)) != waitSize2 { - return -1 // shouldn't happen for audio - } - - packetData = make([]byte, waitSize2) - copy(packetData, waitData2) - - default: - return 0 - } - - // fix Dafang bug (timestamp in seconds) - binary.LittleEndian.PutUint64(packetData[16:], uint64(time.Now().UnixMilli())) - - select { - case c.rawPkt <- packetData: - default: - c.err = fmt.Errorf("%s: media queue is full", "tutk") - return -1 - } - return msgMediaFrame - - case 0x00: - switch cmd[1] { - case 0x70: - _ = c.WriteCh0(c.msgAck0070(cmd)) - select { - case c.rawCmd <- cmd[24:]: - default: - } - return msgCommand - case 0x12: - _ = c.WriteCh0(c.msgAck0012(cmd)) - return msgDafang0012 - case 0x71: - if c.cmdAck != nil { - c.cmdAck() - } - return msgDafang0071 - } - } - - return 0 - } -} - -func (c *Conn) msgAck0070(msg28 []byte) []byte { - // <- 00700800010000000000000000000000340000007625a02f ... - // -> 00710800010000000000000000000000000000007625a02f - msg := c.msg(msgHhrSize + cmdHdrSize) - - cmd := msg[msgHhrSize:] - copy(cmd, "\x00\x71\x0b\x00") - binary.LittleEndian.PutUint16(cmd[4:], c.seqSendCmd1) - c.seqSendCmd1++ - copy(cmd[8:], msg28[8:10]) - - return msg -} - -func (c *Conn) msgAck0012(msg28 []byte) []byte { - // <- 001208000000000000000000000000000c00000000000000 020000000100000001000000 - // -> 00130b000000000000000000000000001400000000000000 0200000001000000010000000000000000000000 - const dataSize = 20 - msg := c.msg(msgHhrSize + cmdHdrSize + dataSize) - - cmd := msg[msgHhrSize:] - copy(cmd, "\x00\x13\x0b\x00") - cmd[16] = dataSize - - data := cmd[cmdHdrSize:] - copy(data, msg28[cmdHdrSize:]) - - return msg -} From 9a1eac8ef4d87ba8042f291a9cb1960d600d2e92 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 17 Jan 2026 09:41:41 +0300 Subject: [PATCH 5/6] Add cache to xiaomi cloud logins --- internal/xiaomi/xiaomi.go | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/internal/xiaomi/xiaomi.go b/internal/xiaomi/xiaomi.go index a27cb9f6..d8611981 100644 --- a/internal/xiaomi/xiaomi.go +++ b/internal/xiaomi/xiaomi.go @@ -50,18 +50,26 @@ func Init() { } var tokens map[string]string -var tokensMu sync.Mutex +var clouds map[string]*xiaomi.Cloud +var cloudsMu sync.Mutex func getCloud(userID string) (*xiaomi.Cloud, error) { - tokensMu.Lock() - defer tokensMu.Unlock() + cloudsMu.Lock() + defer cloudsMu.Unlock() - token := tokens[userID] - cloud := xiaomi.NewCloud(AppXiaomiHome) - if err := cloud.LoginWithToken(userID, token); err != nil { - return nil, err + if cloud := clouds[userID]; cloud != nil { + return cloud, nil } + cloud := xiaomi.NewCloud(AppXiaomiHome) + if err := cloud.LoginWithToken(userID, tokens[userID]); err != nil { + return nil, err + } + if clouds == nil { + clouds = map[string]*xiaomi.Cloud{userID: cloud} + } else { + clouds[userID] = cloud + } return cloud, nil } @@ -221,12 +229,12 @@ func apiDeviceList(w http.ResponseWriter, r *http.Request) { user := query.Get("id") if user == "" { - tokensMu.Lock() + cloudsMu.Lock() users := make([]string, 0, len(tokens)) for s := range tokens { users = append(users, s) } - tokensMu.Unlock() + cloudsMu.Unlock() api.ResponseJSON(w, users) return @@ -308,13 +316,13 @@ func apiAuth(w http.ResponseWriter, r *http.Request) { userID, token := auth.UserToken() auth = nil - tokensMu.Lock() + cloudsMu.Lock() if tokens == nil { tokens = map[string]string{userID: token} } else { tokens[userID] = token } - tokensMu.Unlock() + cloudsMu.Unlock() err = app.PatchConfig([]string{"xiaomi", userID}, token) } From 425fcffbe1f1fa43055ed27513a86df8362c8c29 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 17 Jan 2026 10:06:13 +0300 Subject: [PATCH 6/6] Code refactoring for xiaomi source --- internal/xiaomi/xiaomi.go | 6 +++--- pkg/tutk/session25.go | 4 ---- pkg/xiaomi/legacy/client.go | 11 +---------- pkg/xiaomi/legacy/producer.go | 3 +-- pkg/xiaomi/miss/cs2/conn.go | 30 +++++++++--------------------- 5 files changed, 14 insertions(+), 40 deletions(-) diff --git a/internal/xiaomi/xiaomi.go b/internal/xiaomi/xiaomi.go index d8611981..1801fa86 100644 --- a/internal/xiaomi/xiaomi.go +++ b/internal/xiaomi/xiaomi.go @@ -99,12 +99,12 @@ func getCameraURL(url *url.URL) (string, error) { // The getMissURL request has a fallback to getP2PURL. // But for known models we can save one request to the cloud. if xiaomi.IsLegacy(model) { - return getP2PURL(url) + return getLegacyURL(url) } return getMissURL(url) } -func getP2PURL(url *url.URL) (string, error) { +func getLegacyURL(url *url.URL) (string, error) { query := url.Query() clientPublic, clientPrivate, err := crypto.GenerateKey() @@ -161,7 +161,7 @@ func getMissURL(url *url.URL) (string, error) { res, err := cloudUserRequest(url.User, "/v2/device/miss_get_vendor", params) if err != nil { if strings.Contains(err.Error(), "no available vendor support") { - return getP2PURL(url) + return getLegacyURL(url) } return "", err } diff --git a/pkg/tutk/session25.go b/pkg/tutk/session25.go index a12f52f3..fd1f16b4 100644 --- a/pkg/tutk/session25.go +++ b/pkg/tutk/session25.go @@ -231,10 +231,6 @@ func (s *Session25) msgAckCounters() []byte { } func (s *Session25) handleCh1(cmd []byte) int { - // Channel 1 used for two-way audio. It's important: - // - answer on 0000 command with exact config response (can't set simple proto) - // - send 0012 command at start - // - respond on every 0008 command for smooth playback switch cid := string(cmd[:2]); cid { case "\x00\x00": // client start return msgClientStart diff --git a/pkg/xiaomi/legacy/client.go b/pkg/xiaomi/legacy/client.go index cf84b6fe..a35592d4 100644 --- a/pkg/xiaomi/legacy/client.go +++ b/pkg/xiaomi/legacy/client.go @@ -85,10 +85,7 @@ func xiaofangLogin(conn *tutk.Conn, password string) error { } _, data, err = conn.ReadCommand() - if err != nil { - return err - } - return nil + return err } type Client struct { @@ -110,14 +107,8 @@ func (c *Client) ReadPacket() (hdr, payload []byte, err error) { switch hdr[0] { case tutk.CodecH264, tutk.CodecH265: payload, err = DecodeVideo(payload, c.key) - if err != nil { - return - } case tutk.CodecAAC: payload, err = crypto.Decode(payload, c.key) - if err != nil { - return - } } } return diff --git a/pkg/xiaomi/legacy/producer.go b/pkg/xiaomi/legacy/producer.go index 6669d837..5c1f795d 100644 --- a/pkg/xiaomi/legacy/producer.go +++ b/pkg/xiaomi/legacy/producer.go @@ -62,7 +62,7 @@ func probe(client *Client) ([]*core.Media, error) { var vcodec, acodec *core.Codec for { - // 0 5000 + // 0 5000 codec // 2 0000 codec params // 4 01 active clients // 5 34 unknown const @@ -83,7 +83,6 @@ func probe(client *Client) ([]*core.Media, error) { if codec == tutk.CodecH264 { if h264.NALUType(avcc) == h264.NALUTypeSPS { vcodec = h264.AVCCToCodec(avcc) - vcodec.FmtpLine = "" } } else { if h265.NALUType(avcc) == h265.NALUTypeVPS { diff --git a/pkg/xiaomi/miss/cs2/conn.go b/pkg/xiaomi/miss/cs2/conn.go index 52a70fa1..1bea07c6 100644 --- a/pkg/xiaomi/miss/cs2/conn.go +++ b/pkg/xiaomi/miss/cs2/conn.go @@ -21,7 +21,7 @@ func Dial(host, transport string) (*Conn, error) { _, isTCP := conn.(*tcpConn) c := &Conn{ - conn: conn, + Conn: conn, isTCP: isTCP, channels: [4]*dataChannel{ newDataChannel(0, 10), nil, newDataChannel(250, 100), nil, @@ -32,7 +32,7 @@ func Dial(host, transport string) (*Conn, error) { } type Conn struct { - conn net.Conn + net.Conn isTCP bool err error @@ -116,7 +116,7 @@ func (c *Conn) worker() { buf := make([]byte, 1200) for { - n, err := c.conn.Read(buf) + n, err := c.Conn.Read(buf) if err != nil { c.err = fmt.Errorf("%s: %w", "cs2", err) return @@ -136,7 +136,7 @@ func (c *Conn) worker() { // For TCP we should send ping every second to keep connection alive. // Based on PCAP analysis: official Mi Home app sends PING every ~1s. if now := time.Now(); now.After(keepaliveTS) { - _, _ = c.conn.Write([]byte{magic, msgPing, 0, 0}) + _, _ = c.Conn.Write([]byte{magic, msgPing, 0, 0}) keepaliveTS = now.Add(time.Second) } @@ -151,7 +151,7 @@ func (c *Conn) worker() { if pushed >= 0 { // For UDP we should send ACK. ack := []byte{magic, msgDrwAck, 0, 6, magicDrw, ch, 0, 1, seqHI, seqLO} - _, _ = c.conn.Write(ack) + _, _ = c.Conn.Write(ack) } } @@ -161,7 +161,7 @@ func (c *Conn) worker() { } case msgPing: - _, _ = c.conn.Write([]byte{magic, msgPong, 0, 0}) + _, _ = c.Conn.Write([]byte{magic, msgPong, 0, 0}) case msgPong, msgP2PRdyUDP, msgP2PRdyTCP, msgClose: // skip it case msgDrwAck: // only for UDP if c.cmdAck != nil { @@ -184,18 +184,6 @@ func (c *Conn) Version() string { return "CS2" } -func (c *Conn) RemoteAddr() net.Addr { - return c.conn.RemoteAddr() -} - -func (c *Conn) SetDeadline(t time.Time) error { - return c.conn.SetDeadline(t) -} - -func (c *Conn) Close() error { - return c.conn.Close() -} - func (c *Conn) Error() error { if c.err != nil { return c.err @@ -221,7 +209,7 @@ func (c *Conn) WriteCommand(cmd uint32, data []byte) error { c.seqCh0++ if c.isTCP { - _, err := c.conn.Write(req) + _, err := c.Conn.Write(req) return err } @@ -237,7 +225,7 @@ func (c *Conn) WriteCommand(cmd uint32, data []byte) error { } for { - if _, err := c.conn.Write(req); err != nil { + if _, err := c.Conn.Write(req); err != nil { return err } <-timeout.C @@ -278,7 +266,7 @@ func (c *Conn) WritePacket(hdr, payload []byte) error { copy(req[offset:], hdr) copy(req[offset+hdrSize:], hdr) - _, err := c.conn.Write(req) + _, err := c.Conn.Write(req) return err }