install go2rtc on bob

This commit is contained in:
2026-04-04 19:36:14 +02:00
parent f0b56e63d1
commit ccf88187b8
537 changed files with 69213 additions and 0 deletions
@@ -0,0 +1,271 @@
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 == ModelMijia || model == ModelXiaobai {
username = "admin"
password = query.Get("password")
} else if model == ModelDafang || 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 == ModelDafang || 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()
return err
}
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 {
if c.model == ModelAqaraG2 && hdr[0] == tutk.CodecH265 {
payload, err = DecodeVideo(payload, c.key)
} else {
// ModelAqaraG2: audio AAC
// ModelIMILABA1: video HEVC, audio PCMA
payload, err = crypto.Decode(payload, c.key)
}
}
return
}
const (
cmdVideoStart = 0x01ff
cmdVideoStop = 0x02ff
cmdAudioStart = 0x0300
cmdAudioStop = 0x0301
cmdStreamCtrlReq = 0x0320
)
func (c *Client) WriteCommandJSON(ctrlType uint32, format string, a ...any) error {
if len(a) > 0 {
format = fmt.Sprintf(format, a...)
}
return c.WriteCommand(ctrlType, []byte(format))
}
func (c *Client) StartMedia(video, audio string) error {
switch c.model {
case ModelAqaraG2:
// 0 - 1920x1080, 1 - 1280x720, 2 - ?
switch video {
case "", "fhd":
video = "0"
case "hd":
video = "1"
case "sd":
video = "2"
}
return errors.Join(
c.WriteCommandJSON(cmdVideoStart, `{}`),
c.WriteCommandJSON(0x0605, `{"channel":%s}`, video),
c.WriteCommandJSON(0x0704, `{}`), // don't know why
)
case ModelIMILABA1, ModelMijia:
// 0 - auto, 1 - low, 3 - hd
switch video {
case "", "hd":
video = "3"
case "sd":
video = "1" // 2 is also low quality
case "auto":
video = "0"
}
// quality after start
return errors.Join(
c.WriteCommandJSON(cmdAudioStart, `{}`),
c.WriteCommandJSON(cmdVideoStart, `{}`),
c.WriteCommandJSON(cmdStreamCtrlReq, `{"videoquality":%s}`, video),
)
case ModelXiaobai:
// 00030000 7b7d audio on
// 01030000 7b7d audio off
// 20030000 0000000001000000 fhd (1920x1080)
// 20030000 0000000002000000 hd (1280x720)
// 20030000 0000000004000000 low (640x360)
// 20030000 00000000ff000000 auto (1920x1080)
// ff010000 7b7d video tart
// ff020000 7b7d video stop
var b byte
switch video {
case "", "fhd":
b = 1
case "hd":
b = 2
case "sd":
b = 4
case "auto":
b = 0xff
}
// quality before start
return errors.Join(
c.WriteCommandJSON(cmdAudioStart, `{}`),
c.WriteCommand(cmdStreamCtrlReq, []byte{0, 0, 0, 0, b, 0, 0, 0}),
c.WriteCommandJSON(cmdVideoStart, `{}`),
)
case ModelDafang, 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
}
return fmt.Errorf("xiaomi: unsupported model: %s", c.model)
}
func (c *Client) StopMedia() error {
return errors.Join(
c.WriteCommandJSON(cmdVideoStop, `{}`),
c.WriteCommand(cmdVideoStop, 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.Uint32(data[9:])
i2 := binary.LittleEndian.Uint32(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"
ModelIMILABA1 = "chuangmi.camera.ipc019e"
ModelLoockV1 = "loock.cateye.v01"
ModelXiaobai = "chuangmi.camera.xiaobai"
ModelXiaofang = "isa.camera.isc5"
// ModelMijia support miss format for new fw and legacy format for old fw
ModelMijia = "chuangmi.camera.v2"
// ModelDafang support miss format for new fw and legacy format for old fw
ModelDafang = "isa.camera.df3"
)
func Supported(model string) bool {
switch model {
case ModelAqaraG2, ModelIMILABA1, ModelLoockV1, ModelXiaobai, ModelXiaofang:
return true
}
return false
}