install go2rtc on bob
This commit is contained in:
@@ -0,0 +1,338 @@
|
||||
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"
|
||||
// ModelXiaofang looks like it has the same firmware as the ModelDafang.
|
||||
// There is also an older model "isa.camera.isc5" that only works with the legacy protocol.
|
||||
ModelXiaofang = "isa.camera.isc5c1"
|
||||
)
|
||||
|
||||
func (c *Client) StartMedia(channel, quality, audio string) error {
|
||||
switch c.model {
|
||||
case ModelDafang, ModelXiaofang:
|
||||
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)
|
||||
switch channel {
|
||||
case "", "0":
|
||||
data = fmt.Appendf(data, `{"videoquality":%s,"enableaudio":%s}`, quality, audio)
|
||||
default:
|
||||
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, ModelXiaofang, "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, ModelXiaofang, 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 (p *Packet) SampleRate() uint32 {
|
||||
// flag: 1 0011 000 - sample rate 16000
|
||||
// flag: 100 00 01 0000 000 - sample rate 8000
|
||||
v := (p.Flags >> 3) & 0b1111
|
||||
if v != 0 {
|
||||
return 16000
|
||||
}
|
||||
return 8000
|
||||
}
|
||||
|
||||
//func (p *Packet) AudioUnknown1() byte {
|
||||
// return byte((p.Flags >> 7) & 0b11)
|
||||
//}
|
||||
//
|
||||
//func (p *Packet) AudioUnknown2() byte {
|
||||
// return byte((p.Flags >> 9) & 0b11)
|
||||
//}
|
||||
|
||||
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)
|
||||
//}
|
||||
Reference in New Issue
Block a user