Big rewrite tutk proto support

This commit is contained in:
Alex X
2026-01-04 00:09:32 +03:00
parent d4dc670cb5
commit 6b4eb8ffb6
25 changed files with 2376 additions and 1383 deletions
+5
View File
@@ -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`
+82
View File
@@ -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"`
}
+20 -6
View File
@@ -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
```
+88 -19
View File
@@ -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
}
+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
}
@@ -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:]
}
}
+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
}
+341
View File
@@ -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
}
}
+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
}
+227
View File
@@ -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
}
+217
View File
@@ -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()
}
@@ -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)
}
}
}
+197 -161
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"), 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)
//}
@@ -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
+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 -215
View File
@@ -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)
}
-63
View File
@@ -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
```
-262
View File
@@ -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
}
-251
View File
@@ -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
}
-191
View File
@@ -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
}
-192
View File
@@ -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
}