Merge branch 'AlexxIT:master' into wyze

This commit is contained in:
seydx
2026-01-17 09:40:49 +01:00
committed by GitHub
21 changed files with 2402 additions and 936 deletions
+11
View File
@@ -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())
}
+264
View File
@@ -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
}
@@ -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]
@@ -136,3 +139,39 @@ func swap(src, dst []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:]
}
}
+14
View File
@@ -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))
}
+28
View File
@@ -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
}
+163
View File
@@ -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
}
+378
View File
@@ -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
}
+337
View File
@@ -0,0 +1,337 @@
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 {
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
}
}
+68
View File
@@ -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
}
+218
View File
@@ -0,0 +1,218 @@
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()
return err
}
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)
case tutk.CodecAAC:
payload, err = crypto.Decode(payload, c.key)
}
}
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
}
+216
View File
@@ -0,0 +1,216 @@
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 codec
// 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)
}
} 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()
}
@@ -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,7 +22,7 @@ func (p *Producer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv
case core.CodecPCMA:
var buf []byte
if p.model == "isa.camera.hlc6" {
if p.client.SpeakerCodec() == codecPCM {
dst := &core.Codec{Name: core.CodecPCML, ClockRate: 8000}
transcode := pcm.Transcode(dst, track.Codec)
@@ -31,7 +30,8 @@ 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.client.WriteAudio(miss.CodecPCM, buf[:size])
p.Send += size
_ = p.client.WriteAudio(codecPCM, buf[:size])
buf = buf[size:]
}
}
@@ -40,13 +40,14 @@ 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.client.WriteAudio(miss.CodecPCMA, buf[:size])
p.Send += 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 {
@@ -54,13 +55,15 @@ 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.client.WriteAudio(miss.CodecOPUS, buf)
p.Send += len(buf)
_ = p.client.WriteAudio(codecOPUS, buf)
buf = nil
}
}
} else {
sender.Handler = func(pkt *rtp.Packet) {
_ = p.client.WriteAudio(miss.CodecOPUS, pkt.Payload)
p.Send += len(pkt.Payload)
_ = p.client.WriteAudio(codecOPUS, pkt.Payload)
}
}
}
+193 -175
View File
@@ -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"))
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,28 +277,40 @@ type Packet struct {
Payload []byte
}
func (p *Packet) SampleRate() uint32 {
// flag: 1 0011 000 - sample rate 16000
// flag: 100 00 01 0000 000 - sample rate 8000
v := (p.Flags >> 3) & 0b1111
if v != 0 {
return 16000
}
return 8000
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
}
//func (p *Packet) AudioUnknown1() byte {
// return byte((p.Flags >> 7) & 0b11)
// 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 (p *Packet) AudioUnknown2() byte {
// return byte((p.Flags >> 9) & 0b11)
//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)
//}
func GenerateKey() ([]byte, []byte, error) {
public, private, err := box.GenerateKey(rand.Reader)
if err != nil {
return nil, nil, err
}
return public[:], private[:], err
}
@@ -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 {
@@ -180,16 +180,8 @@ func (c *Conn) Protocol() string {
return "cs2+udp"
}
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) Version() string {
return "CS2"
}
func (c *Conn) Error() error {
@@ -199,25 +191,25 @@ 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.channels[0].Pop()
if !ok {
return 0, nil, c.Error()
}
cmd = binary.LittleEndian.Uint16(buf[:2])
cmd = binary.LittleEndian.Uint32(buf)
data = buf[4:]
return
}
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 {
_, err := c.conn.Write(req)
_, err := c.Conn.Write(req)
return err
}
@@ -233,7 +225,7 @@ func (c *Conn) WriteCommand(cmd uint16, data []byte) error {
}
for {
if _, err := c.conn.Write(req); err != nil {
if _, err := c.Conn.Write(req); err != nil {
return err
}
<-timeout.C
@@ -247,18 +239,20 @@ func (c *Conn) WriteCommand(cmd uint16, data []byte) error {
}
}
func (c *Conn) ReadPacket() ([]byte, error) {
const hdrSize = 32
func (c *Conn) ReadPacket() (hdr, payload []byte, err error) {
data, ok := c.channels[2].Pop()
if !ok {
return nil, c.Error()
return nil, nil, c.Error()
}
return data, nil
return data[:hdrSize], data[hdrSize:], nil
}
func (c *Conn) WritePacket(data []byte) error {
func (c *Conn) WritePacket(hdr, payload []byte) error {
const offset = 12
n := uint32(len(data))
n := hdrSize + uint32(len(payload))
req := make([]byte, n+offset)
req[0] = magic
req[1] = msgDrw
@@ -269,9 +263,10 @@ 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+hdrSize:], hdr)
_, err := c.conn.Write(req)
_, err := c.Conn.Write(req)
return err
}
+204
View File
@@ -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)
}
+9 -216
View File
@@ -1,230 +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(core.ProbeTimeout))
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: pkt.SampleRate()}
}
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},
})
switch client.Protocol() {
case "cs2+udp", "cs2+tcp":
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(core.ConnDeadline))
pkt, err := p.client.ReadPacket()
if err != nil {
return err
}
// 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)
}
-461
View File
@@ -1,461 +0,0 @@
package tutk
import (
"bytes"
"crypto/rand"
"encoding/binary"
"fmt"
"io"
"net"
"sync"
"sync/atomic"
"time"
)
func Dial(host, uid 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(),
}
if err = c.handshake([]byte(uid)); err != nil {
_ = c.Close()
return nil, err
}
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
seqCh0 uint16
seqCmd uint16
rawCmd chan []byte
rawPkt chan []byte
cmdMu sync.Mutex
cmdAck func()
}
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
func(_, res []byte) bool {
return bytes.Index(res, uid) == 16 // 02061200
},
); err != nil {
return err
}
if err = c.Write(c.msgLanSearch(uid, 2)); err != nil {
return err
}
if _, err = c.WriteAndWait(
c.msgAvClientStartReq(), // 07042100 + 00000b00
func(req, res []byte) bool {
mid := req[48:52]
return bytes.Index(res, mid) == 48 // 08041200 + 00140800
},
); 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)
var waitSeq uint16
var waitSize uint32
var waitData []byte
for {
n, addr, err := c.conn.ReadFromUDP(buf)
if err != nil {
c.err = fmt.Errorf("%s: %w", "tutk", err)
return
}
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])
}
}
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() {
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.conn.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) {
c.addr.Port = addr.Port
return res, 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)
}
req := c.msgAvSendIOCtrl(cmd, data)
for {
if err := c.Write(req); 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 {
panic("not implemented")
}
func genSID() []byte {
b := make([]byte, 16)
_, _ = rand.Read(b[8:])
copy(b, b[8:10])
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)
}