Big rewrite tutk proto support
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user