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
+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
}
-141
View File
@@ -1,141 +0,0 @@
package tutk
import (
"encoding/binary"
"math/bits"
)
// 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(dst, src []byte) []byte {
n := len(src)
tmp := make([]byte, n)
if len(dst) < n {
dst = make([]byte, n)
}
src16 := src
tmp16 := tmp
dst16 := dst
for ; n >= 16; n -= 16 {
for i := 0; i != 16; i += 4 {
x := binary.LittleEndian.Uint32(src16[i:])
binary.LittleEndian.PutUint32(tmp16[i:], bits.RotateLeft32(x, i+3))
}
swap(dst16, tmp16, 16)
for i := 0; i != 16; i++ {
tmp16[i] = dst16[i] ^ charlie[i]
}
for i := 0; i != 16; i += 4 {
x := binary.LittleEndian.Uint32(tmp16[i:])
binary.LittleEndian.PutUint32(dst16[i:], bits.RotateLeft32(x, i+1))
}
tmp16 = tmp16[16:]
dst16 = dst16[16:]
src16 = src16[16:]
}
swap(tmp16, src16, n)
for i := 0; i < n; i++ {
dst16[i] = tmp16[i] ^ charlie[i]
}
return dst
}
func TransCodePartial(dst, src []byte) []byte {
n := len(src)
tmp := make([]byte, n)
if len(dst) < n {
dst = make([]byte, n)
}
src16 := src
tmp16 := tmp
dst16 := dst
for ; n >= 16; n -= 16 {
for i := 0; i != 16; i += 4 {
x := binary.LittleEndian.Uint32(src16[i:])
binary.LittleEndian.PutUint32(tmp16[i:], bits.RotateLeft32(x, -i-1))
}
for i := 0; i != 16; i++ {
dst16[i] = tmp16[i] ^ charlie[i]
}
swap(tmp16, dst16, 16)
for i := 0; i != 16; i += 4 {
x := binary.LittleEndian.Uint32(tmp16[i:])
binary.LittleEndian.PutUint32(dst16[i:], bits.RotateLeft32(x, -i-3))
}
tmp16 = tmp16[16:]
dst16 = dst16[16:]
src16 = src16[16:]
}
for i := 0; i < n; i++ {
tmp16[i] = src16[i] ^ charlie[i]
}
swap(dst16, tmp16, n)
return dst
}
func swap(dst, src []byte, n int) {
switch n {
case 2:
_, _ = src[1], dst[1]
dst[0] = src[1]
dst[1] = src[0]
return
case 4:
_, _ = src[3], dst[3]
dst[0] = src[2]
dst[1] = src[3]
dst[2] = src[0]
dst[3] = src[1]
return
case 8:
_, _ = src[7], dst[7]
dst[0] = src[7]
dst[1] = src[4]
dst[2] = src[3]
dst[3] = src[2]
dst[4] = src[1]
dst[5] = src[6]
dst[6] = src[5]
dst[7] = src[0]
return
case 16:
_, _ = src[15], dst[15]
dst[0] = src[11]
dst[1] = src[9]
dst[2] = src[8]
dst[3] = src[15]
dst[4] = src[13]
dst[5] = src[10]
dst[6] = src[12]
dst[7] = src[14]
dst[8] = src[2]
dst[9] = src[1]
dst[10] = src[5]
dst[11] = src[0]
dst[12] = src[6]
dst[13] = src[4]
dst[14] = src[7]
dst[15] = src[3]
return
}
copy(dst, src[:n])
}
-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
}