mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2026-04-22 23:57:20 +08:00
317 lines
7.0 KiB
Go
317 lines
7.0 KiB
Go
package miss
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/AlexxIT/go2rtc/pkg/tutk"
|
|
"github.com/AlexxIT/go2rtc/pkg/xiaomi/crypto"
|
|
"github.com/AlexxIT/go2rtc/pkg/xiaomi/miss/cs2"
|
|
)
|
|
|
|
const (
|
|
codecH264 = 4
|
|
codecH265 = 5
|
|
codecPCM = 1024
|
|
codecPCMU = 1026
|
|
codecPCMA = 1027
|
|
codecOPUS = 1032
|
|
)
|
|
|
|
type Conn interface {
|
|
Protocol() string
|
|
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
|
|
key []byte
|
|
model string
|
|
}
|
|
|
|
const (
|
|
cmdAuthReq = 0x100
|
|
cmdAuthRes = 0x101
|
|
cmdVideoStart = 0x102
|
|
cmdVideoStop = 0x103
|
|
cmdAudioStart = 0x104
|
|
cmdAudioStop = 0x105
|
|
cmdSpeakerStartReq = 0x106
|
|
cmdSpeakerStartRes = 0x107
|
|
cmdSpeakerStop = 0x108
|
|
cmdStreamCtrlReq = 0x109
|
|
cmdStreamCtrlRes = 0x10A
|
|
cmdGetAudioFormatReq = 0x10B
|
|
cmdGetAudioFormatRes = 0x10C
|
|
cmdPlaybackReq = 0x10D
|
|
cmdPlaybackRes = 0x10E
|
|
cmdDevInfoReq = 0x110
|
|
cmdDevInfoRes = 0x111
|
|
cmdMotorReq = 0x112
|
|
cmdMotorRes = 0x113
|
|
cmdEncoded = 0x1001
|
|
)
|
|
|
|
func login(conn Conn, clientPublic, sign string) error {
|
|
s := fmt.Sprintf(`{"public_key":"%s","sign":"%s","uuid":"","support_encrypt":0}`, clientPublic, sign)
|
|
if err := conn.WriteCommand(cmdAuthReq, []byte(s)); err != nil {
|
|
return err
|
|
}
|
|
|
|
_, data, err := conn.ReadCommand()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !bytes.Contains(data, []byte(`"result":"success"`)) {
|
|
return fmt.Errorf("miss: auth: %s", data)
|
|
}
|
|
|
|
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 := crypto.Encode(data, c.key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.Conn.WriteCommand(cmdEncoded, data)
|
|
}
|
|
|
|
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 == "" {
|
|
data = fmt.Appendf(data, `{"videoquality":%s,"enableaudio":%s}`, quality, audio)
|
|
} else {
|
|
data = fmt.Appendf(data, `{"videoquality":-1,"videoquality2":%s,"enableaudio":%s}`, quality, audio)
|
|
}
|
|
return c.WriteCommand(data)
|
|
}
|
|
|
|
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) 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) {
|
|
hdr, payload, err := c.Conn.ReadPacket()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("miss: read media: %w", err)
|
|
}
|
|
|
|
if len(hdr) < hdrSize {
|
|
return nil, fmt.Errorf("miss: packet header too small")
|
|
}
|
|
|
|
payload, err = crypto.Decode(payload, c.key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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 := crypto.Encode(payload, c.key) // new payload will have new size!
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
n := uint32(len(payload))
|
|
|
|
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 {
|
|
//Length uint32
|
|
CodecID uint32
|
|
Sequence uint32
|
|
Flags uint32
|
|
Timestamp uint64 // msec
|
|
//TimestampS uint32
|
|
//Reserved uint32
|
|
Payload []byte
|
|
}
|
|
|
|
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)
|
|
//}
|