Add support xiaomi source

This commit is contained in:
Alex X
2025-12-14 13:07:45 +03:00
parent 17c1f69f66
commit a4d4598a13
9 changed files with 1678 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
package xiaomi
import (
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/xiaomi/miss"
"github.com/pion/rtp"
)
const size8bit40ms = 8000 * 0.040
func (p *Producer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
if err := p.client.SpeakerStart(); err != nil {
return err
}
// TODO: check this!!!
time.Sleep(time.Second)
sender := core.NewSender(media, track.Codec)
switch track.Codec.Name {
case core.CodecPCMA:
var buf []byte
sender.Handler = func(pkt *rtp.Packet) {
buf = append(buf, pkt.Payload...)
for len(buf) >= size8bit40ms {
_ = p.client.WriteAudio(miss.CodecPCMA, buf[:size8bit40ms])
buf = buf[size8bit40ms:]
}
}
case core.CodecOpus:
sender.Handler = func(pkt *rtp.Packet) {
_ = p.client.WriteAudio(miss.CodecOPUS, pkt.Payload)
}
}
sender.HandleRTP(track)
p.Senders = append(p.Senders, sender)
return nil
}
+563
View File
@@ -0,0 +1,563 @@
package xiaomi
import (
"bytes"
"crypto/md5"
"crypto/rand"
"crypto/rc4"
"crypto/sha1"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
)
type Cloud struct {
client *http.Client
sid string
cookies string // for auth
ssecurity []byte // for encryption
userID string
passToken string
auth map[string]string
}
func NewCloud(sid string) *Cloud {
return &Cloud{
client: &http.Client{Timeout: 15 * time.Second},
sid: sid,
}
}
func (c *Cloud) Login(username, password string) error {
res, err := c.client.Get("https://account.xiaomi.com/pass/serviceLogin?_json=true&sid=" + c.sid)
if err != nil {
return err
}
var v1 struct {
Qs string `json:"qs"`
Sign string `json:"_sign"`
Sid string `json:"sid"`
Callback string `json:"callback"`
}
if _, err = readLoginResponse(res.Body, &v1); err != nil {
return err
}
hash := fmt.Sprintf("%X", md5.Sum([]byte(password)))
form := url.Values{
"_json": {"true"},
"hash": {hash},
"sid": {v1.Sid},
"callback": {v1.Callback},
"_sign": {v1.Sign},
"qs": {v1.Qs},
"user": {username},
}
cookies := "deviceId=" + core.RandString(16, 62)
// login after captcha
if c.auth != nil && c.auth["captcha_code"] != "" {
form.Set("captCode", c.auth["captcha_code"])
cookies += "; ick=" + c.auth["ick"]
}
req := Request{
Method: "POST",
URL: "https://account.xiaomi.com/pass/serviceLoginAuth2",
RawBody: form.Encode(),
Headers: url.Values{
"Content-Type": {"application/x-www-form-urlencoded"},
},
RawCookies: cookies,
}.Encode()
res, err = c.client.Do(req)
if err != nil {
return err
}
var v2 struct {
Ssecurity []byte `json:"ssecurity"`
PassToken string `json:"passToken"`
Location string `json:"location"`
CaptchaURL string `json:"captchaURL"`
NotificationURL string `json:"notificationUrl"`
}
body, err := readLoginResponse(res.Body, &v2)
if err != nil {
return err
}
// save auth for two step verification
c.auth = map[string]string{
"username": username,
"password": password,
}
if v2.CaptchaURL != "" {
return c.getCaptcha(v2.CaptchaURL)
}
if v2.NotificationURL != "" {
return c.authStart(v2.NotificationURL)
}
if v2.Location == "" {
return fmt.Errorf("xiaomi: %s", body)
}
c.auth = nil
c.ssecurity = v2.Ssecurity
c.passToken = v2.PassToken
return c.finishAuth(v2.Location)
}
func (c *Cloud) LoginWithCaptcha(captcha string) error {
if c.auth == nil || c.auth["ick"] == "" {
panic("wrong login step")
}
c.auth["captcha_code"] = captcha
// check if captcha after verify
if c.auth["flag"] != "" {
return c.sendTicket()
}
return c.Login(c.auth["username"], c.auth["password"])
}
func (c *Cloud) LoginWithVerify(ticket string) error {
if c.auth == nil || c.auth["flag"] == "" {
panic("wrong login step")
}
req := Request{
Method: "POST",
URL: "https://account.xiaomi.com/identity/auth/verify" + c.verifyName(),
RawParams: "_flag" + c.auth["flag"] + "&ticket=" + ticket + "&trust=false&_json=true",
RawCookies: "identity_session=" + c.auth["identity_session"],
}.Encode()
res, err := c.client.Do(req)
if err != nil {
return err
}
var v1 struct {
Location string `json:"location"`
}
body, err := readLoginResponse(res.Body, &v1)
if err != nil {
return err
}
if v1.Location == "" {
return fmt.Errorf("xiaomi: %s", body)
}
return c.finishAuth(v1.Location)
}
func (c *Cloud) getCaptcha(captchaURL string) error {
res, err := c.client.Get("https://account.xiaomi.com" + captchaURL)
if err != nil {
return err
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return err
}
c.auth["ick"] = findCookie(res, "ick")
return &LoginError{
Captcha: body,
}
}
func (c *Cloud) authStart(notificationURL string) error {
rawURL := strings.Replace(notificationURL, "/fe/service/identity/authStart", "/identity/list", 1)
res, err := c.client.Get(rawURL)
if err != nil {
return err
}
var v1 struct {
Code int `json:"code"`
Flag int `json:"flag"`
}
if _, err = readLoginResponse(res.Body, &v1); err != nil {
return err
}
c.auth["flag"] = strconv.Itoa(v1.Flag)
c.auth["identity_session"] = findCookie(res, "identity_session")
return c.sendTicket()
}
func findCookie(res *http.Response, name string) string {
for _, cookie := range res.Cookies() {
if cookie.Name == name {
return cookie.Value
}
}
return ""
}
func (c *Cloud) verifyName() string {
switch c.auth["flag"] {
case "4":
return "Phone"
case "8":
return "Email"
}
return ""
}
func (c *Cloud) sendTicket() error {
name := c.verifyName()
cookies := "identity_session=" + c.auth["identity_session"]
req := Request{
URL: "https://account.xiaomi.com/identity/auth/verify" + name,
RawParams: "_flag=" + c.auth["flag"] + "&_json=true",
RawCookies: cookies,
}.Encode()
res, err := c.client.Do(req)
if err != nil {
return err
}
var v1 struct {
Code int `json:"code"`
MaskedPhone string `json:"maskedPhone"`
MaskedEmail string `json:"maskedEmail"`
}
if _, err = readLoginResponse(res.Body, &v1); err != nil {
return err
}
// verify after captcha
captCode := c.auth["captcha_code"]
if captCode != "" {
cookies += "; ick=" + c.auth["ick"]
}
req = Request{
Method: "POST",
URL: "https://account.xiaomi.com/identity/auth/send" + name + "Ticket",
RawCookies: cookies,
RawBody: `{"retry":0,"icode":"` + captCode + `","_json":"true"}`,
}.Encode()
res, err = c.client.Do(req)
if err != nil {
return err
}
var v2 struct {
Code int `json:"code"`
CaptchaURL string `json:"captchaURL"`
}
body, err := readLoginResponse(res.Body, &v2)
if err != nil {
return err
}
if v2.CaptchaURL != "" {
return c.getCaptcha(v2.CaptchaURL)
}
if v2.Code != 0 {
return fmt.Errorf("xiaomi: %s", body)
}
return &LoginError{
VerifyPhone: v1.MaskedPhone,
VerifyEmail: v1.MaskedEmail,
}
}
type LoginError struct {
Captcha []byte `json:"captcha,omitempty"`
VerifyPhone string `json:"verify_phone,omitempty"`
VerifyEmail string `json:"verify_email,omitempty"`
}
func (l *LoginError) Error() string {
return ""
}
func (c *Cloud) finishAuth(location string) error {
res, err := c.client.Get(location)
if err != nil {
return err
}
defer res.Body.Close()
// LoginWithVerify
// - userId, cUserId, serviceToken from cookies
// - passToken from redirect cookies
// - ssecurity from extra header
// LoginWithToken
// - userId, cUserId, serviceToken from cookies
var cUserId, serviceToken string
for res != nil {
for _, cookie := range res.Cookies() {
switch cookie.Name {
case "userId":
c.userID = cookie.Value
case "cUserId":
cUserId = cookie.Value
case "serviceToken":
serviceToken = cookie.Value
case "passToken":
c.passToken = cookie.Value
}
}
if s := res.Header.Get("Extension-Pragma"); s != "" {
var v1 struct {
Ssecurity []byte `json:"ssecurity"`
}
if err = json.Unmarshal([]byte(s), &v1); err != nil {
return err
}
c.ssecurity = v1.Ssecurity
}
res = res.Request.Response
}
c.cookies = fmt.Sprintf("userId=%s; cUserId=%s; serviceToken=%s", c.userID, cUserId, serviceToken)
return nil
}
func (c *Cloud) LoginWithToken(userID, passToken string) error {
req, err := http.NewRequest("GET", "https://account.xiaomi.com/pass/serviceLogin?_json=true&sid="+c.sid, nil)
if err != nil {
return err
}
req.Header.Set("Cookie", fmt.Sprintf("userId=%s; passToken=%s", userID, passToken))
res, err := c.client.Do(req)
if err != nil {
return err
}
var v1 struct {
Ssecurity []byte `json:"ssecurity"`
PassToken string `json:"passToken"`
Location string `json:"location"`
}
if _, err = readLoginResponse(res.Body, &v1); err != nil {
return err
}
c.ssecurity = v1.Ssecurity
c.passToken = v1.PassToken
return c.finishAuth(v1.Location)
}
func (c *Cloud) UserToken() (string, string) {
return c.userID, c.passToken
}
func (c *Cloud) Request(baseURL, apiURL, params string, headers map[string]string) ([]byte, error) {
form := url.Values{"data": {params}}
nonce := genNonce()
signedNonce := genSignedNonce(c.ssecurity, nonce)
// 1. gen hash for data param
form.Set("rc4_hash__", genSignature64("POST", apiURL, form, signedNonce))
// 2. encrypt data and hash params
for _, v := range form {
ciphertext, err := crypt(signedNonce, []byte(v[0]))
if err != nil {
return nil, err
}
v[0] = base64.StdEncoding.EncodeToString(ciphertext)
}
// 3. add signature for encrypted data and hash params
form.Set("signature", genSignature64("POST", apiURL, form, signedNonce))
// 4. add nonce
form.Set("_nonce", base64.StdEncoding.EncodeToString(nonce))
req, err := http.NewRequest("POST", baseURL+apiURL, strings.NewReader(form.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Cookie", c.cookies)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
for k, v := range headers {
req.Header.Set(k, v)
}
res, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, errors.New(res.Status)
}
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
ciphertext, err := base64.StdEncoding.DecodeString(string(body))
if err != nil {
return nil, err
}
plaintext, err := crypt(signedNonce, ciphertext)
if err != nil {
return nil, err
}
var res1 struct {
Code int `json:"code"`
Message string `json:"message"`
Result json.RawMessage `json:"result"`
}
if err = json.Unmarshal(plaintext, &res1); err != nil {
return nil, err
}
if res1.Code != 0 {
return nil, errors.New("xiaomi: " + res1.Message)
}
return res1.Result, nil
}
func readLoginResponse(rc io.ReadCloser, v any) ([]byte, error) {
defer rc.Close()
body, err := io.ReadAll(rc)
if err != nil {
return nil, err
}
body, ok := bytes.CutPrefix(body, []byte("&&&START&&&"))
if !ok {
return nil, fmt.Errorf("xiaomi: %s", body)
}
return body, json.Unmarshal(body, &v)
}
func genNonce() []byte {
ts := time.Now().Unix() / 60
nonce := make([]byte, 12)
_, _ = rand.Read(nonce[:8])
binary.BigEndian.PutUint32(nonce[8:], uint32(ts))
return nonce
}
func genSignedNonce(ssecurity, nonce []byte) []byte {
hasher := sha256.New()
hasher.Write(ssecurity)
hasher.Write(nonce)
return hasher.Sum(nil)
}
func crypt(key, plaintext []byte) ([]byte, error) {
cipher, err := rc4.NewCipher(key)
if err != nil {
return nil, err
}
tmp := make([]byte, 1024)
cipher.XORKeyStream(tmp, tmp)
ciphertext := make([]byte, len(plaintext))
cipher.XORKeyStream(ciphertext, plaintext)
return ciphertext, nil
}
func genSignature64(method, path string, values url.Values, signedNonce []byte) string {
s := method + "&" + path + "&data=" + values.Get("data")
if values.Has("rc4_hash__") {
s += "&rc4_hash__=" + values.Get("rc4_hash__")
}
s += "&" + base64.StdEncoding.EncodeToString(signedNonce)
hasher := sha1.New()
hasher.Write([]byte(s))
signature := hasher.Sum(nil)
return base64.StdEncoding.EncodeToString(signature)
}
type Request struct {
Method string
URL string
RawParams string
RawBody string
Headers url.Values
RawCookies string
}
func (r Request) Encode() *http.Request {
if r.RawParams != "" {
r.URL += "?" + r.RawParams
}
var body io.Reader
if r.RawBody != "" {
body = strings.NewReader(r.RawBody)
}
req, err := http.NewRequest(r.Method, r.URL, body)
if err != nil {
return nil
}
if r.Headers != nil {
req.Header = http.Header(r.Headers)
}
if r.RawCookies != "" {
req.Header.Set("Cookie", r.RawCookies)
}
return req
}
+451
View File
@@ -0,0 +1,451 @@
package miss
import (
"crypto/rand"
"encoding/binary"
"encoding/hex"
"fmt"
"log"
"net"
"net/url"
"strings"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
"golang.org/x/crypto/chacha20"
"golang.org/x/crypto/nacl/box"
)
func Dial(rawURL string) (*Client, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
query := u.Query()
if query.Get("vendor") != "cs2" {
return nil, fmt.Errorf("miss: unsupported vendor")
}
clientPrivate := query.Get("client_private")
devicePublic := query.Get("device_public")
key, err := calcSharedKey(devicePublic, clientPrivate)
if err != nil {
return nil, err
}
conn, err := net.ListenUDP("udp", nil)
if err != nil {
return nil, err
}
client := &Client{
conn: conn,
addr: &net.UDPAddr{IP: net.ParseIP(u.Host), Port: 32108},
buf: make([]byte, 1500),
key: key,
}
clientPublic := query.Get("client_public")
sign := query.Get("sign")
if err = client.login(clientPublic, sign); err != nil {
_ = conn.Close()
return nil, err
}
client.chSeq0 = 1
client.chRaw2 = make(chan []byte, 100)
go client.worker()
return client, nil
}
const (
CodecH264 = 4
CodecH265 = 5
CodecPCM = 1024
CodecPCMU = 1026
CodecPCMA = 1027
CodecOPUS = 1032
)
type Client struct {
conn *net.UDPConn
addr *net.UDPAddr
buf []byte
key []byte // shared key
chSeq0 uint16
chSeq3 uint16
chRaw2 chan []byte
}
func (c *Client) RemoteAddr() *net.UDPAddr {
return c.addr
}
func (c *Client) SetDeadline(t time.Time) error {
return c.conn.SetDeadline(t)
}
func (c *Client) Close() error {
return c.conn.Close()
}
const (
magic = 0xF1
magicDrw = 0xD1
msgLanSearch = 0x30
msgPunchPkt = 0x41
msgP2PRdy = 0x42
msgDrw = 0xD0
msgDrwAck = 0xD1
msgAlive = 0xE0
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 (c *Client) login(clientPublic, sign string) error {
_ = c.conn.SetDeadline(time.Now().Add(core.ConnDialTimeout))
buf, err := c.writeAndWait([]byte{magic, msgLanSearch, 0, 0}, msgPunchPkt)
if err != nil {
return fmt.Errorf("miss: read punch: %w", err)
}
_, err = c.writeAndWait(buf, msgP2PRdy)
if err != nil {
return fmt.Errorf("miss: read ready: %w", err)
}
_, _ = c.conn.WriteToUDP([]byte{magic, msgAlive, 0, 0}, c.addr)
s := fmt.Sprintf(`{"public_key":"%s","sign":"%s","uuid":"","support_encrypt":0}`, clientPublic, sign)
buf, err = c.writeAndWait(marshalCmd(0, 0, cmdAuthReq, []byte(s)), msgDrw)
if err != nil {
return fmt.Errorf("miss: read auth: %w", err)
}
if !strings.Contains(string(buf[16:]), `"result":"success"`) {
return fmt.Errorf("miss: read auth: %s", buf[16:])
}
_, _ = c.conn.WriteToUDP([]byte{magic, msgDrwAck, 0, 6, magicDrw, 0, 0, 1, 0, 0}, c.addr)
_ = c.conn.SetDeadline(time.Time{})
return nil
}
func (c *Client) writeAndWait(b []byte, waitMsg uint8) ([]byte, error) {
if _, err := c.conn.WriteToUDP(b, c.addr); err != nil {
return nil, err
}
for {
n, addr, err := c.conn.ReadFromUDP(c.buf)
if err != nil {
return nil, err
}
if string(addr.IP) != string(c.addr.IP) {
continue // skip messages from another IP
}
if n >= 16 && c.buf[0] == magic && c.buf[1] == waitMsg {
if waitMsg == msgPunchPkt {
c.addr.Port = addr.Port
}
return c.buf[:n], nil
}
}
}
func (c *Client) VideoStart(channel, quality, audio uint8) error {
buf := binary.BigEndian.AppendUint32(nil, cmdVideoStart)
if channel == 0 {
buf = fmt.Appendf(buf, `{"videoquality":%d,"enableaudio":%d}`, quality, audio)
} else {
buf = fmt.Appendf(buf, `{"videoquality":-1,"videoquality2":%d,"enableaudio":%d}`, quality, audio)
}
buf, err := encode(c.key, buf)
if err != nil {
return err
}
buf = marshalCmd(0, c.chSeq0, cmdEncoded, buf)
c.chSeq0++
_, err = c.conn.WriteToUDP(buf, c.addr)
return err
}
func (c *Client) SpeakerStart() error {
buf := binary.BigEndian.AppendUint32(nil, cmdSpeakerStartReq)
buf, err := encode(c.key, buf)
if err != nil {
return err
}
buf = marshalCmd(0, c.chSeq0, cmdEncoded, buf)
c.chSeq0++
_, err = c.conn.WriteToUDP(buf, c.addr)
return err
}
func (c *Client) ReadPacket() (*Packet, error) {
b, ok := <-c.chRaw2
if !ok {
return nil, fmt.Errorf("miss: read raw: i/o timeout")
}
return unmarshalPacket(c.key, b)
}
func unmarshalPacket(key, b []byte) (*Packet, error) {
n := uint32(len(b))
if n < 32 {
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:])
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
}
func (c *Client) WriteAudio(codecID uint32, payload []byte) error {
payload, err := encode(c.key, payload)
if err != nil {
return err
}
n := uint32(len(payload))
const hdrOffset = 12
const hdrSize = 32
buf := make([]byte, n+hdrOffset+hdrSize)
buf[0] = magic
buf[1] = msgDrw
binary.BigEndian.PutUint16(buf[2:], uint16(n+8+hdrSize))
buf[4] = magicDrw
buf[5] = 3 // channel
binary.BigEndian.PutUint16(buf[6:], c.chSeq3)
binary.BigEndian.PutUint32(buf[8:], n+hdrSize)
binary.LittleEndian.PutUint32(buf[hdrOffset:], n)
binary.LittleEndian.PutUint32(buf[hdrOffset+4:], codecID)
binary.LittleEndian.PutUint64(buf[hdrOffset+16:], uint64(time.Now().UnixMilli()))
copy(buf[hdrOffset+hdrSize:], payload)
c.chSeq3++
_, err = c.conn.WriteToUDP(buf, c.addr)
return err
}
func (c *Client) worker() {
defer close(c.chRaw2)
chAck := []uint16{1, 0, 0, 0}
var ch2WaitSize int
var ch2WaitData []byte
for {
n, addr, err := c.conn.ReadFromUDP(c.buf)
if err != nil {
return
}
//log.Printf("<- %.20x...", c.buf[:n])
if string(addr.IP) != string(c.addr.IP) || n < 8 || c.buf[0] != magic {
//log.Printf("unknown msg: %x", c.buf[:n])
continue // skip messages from another IP
}
switch c.buf[1] {
case msgDrw:
ch := c.buf[5]
seqHI := c.buf[6]
seqLO := c.buf[7]
if chAck[ch] != uint16(seqHI)<<8|uint16(seqLO) {
continue
}
chAck[ch]++
//log.Printf("%.40x", c.buf)
ack := []byte{magic, msgDrwAck, 0, 6, magicDrw, ch, 0, 1, seqHI, seqLO}
if _, err = c.conn.WriteToUDP(ack, c.addr); err != nil {
return
}
switch ch {
case 0:
//log.Printf("data ch0 %x", c.buf[:n])
//size := binary.BigEndian.Uint32(c.buf[8:])
//if binary.BigEndian.Uint32(c.buf[12:]) == cmdEncoded {
// raw, _ := decode(c.key, c.buf[16:12+size])
// log.Printf("cmd enc %x", raw)
//} else {
// log.Printf("cmd raw %x", c.buf[12:12+size])
//}
case 2:
ch2WaitData = append(ch2WaitData, c.buf[8:n]...)
for len(ch2WaitData) > 4 {
if ch2WaitSize == 0 {
ch2WaitSize = int(binary.BigEndian.Uint32(ch2WaitData))
ch2WaitData = ch2WaitData[4:]
}
if ch2WaitSize <= len(ch2WaitData) {
c.chRaw2 <- ch2WaitData[:ch2WaitSize]
ch2WaitData = ch2WaitData[ch2WaitSize:]
ch2WaitSize = 0
} else {
break
}
}
default:
log.Printf("!!! unknown chanel: %x", c.buf[:n])
}
case msgDrwAck: // skip it
default:
log.Printf("!!! unknown msg type: %x", c.buf[:n])
}
}
}
func marshalCmd(channel byte, seq uint16, cmd uint32, payload []byte) []byte {
size := len(payload)
buf := make([]byte, 4+4+4+4+size)
// 1. message header (4 bytes)
buf[0] = magic
buf[1] = msgDrw
binary.BigEndian.PutUint16(buf[2:], uint16(4+4+4+size))
// 2. drw? header (4 bytes)
buf[4] = magicDrw
buf[5] = channel
binary.BigEndian.PutUint16(buf[6:], seq)
// 3. payload size (4 bytes)
binary.BigEndian.PutUint32(buf[8:], uint32(4+size))
// 4. payload command (4 bytes)
binary.BigEndian.PutUint32(buf[12:], cmd)
// 5. payload
copy(buf[16:], payload)
return buf
}
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
}
type Packet struct {
//Length uint32
CodecID uint32
Sequence uint32
Flags uint32
Timestamp uint64 // msec
//TimestampS uint32
//Reserved uint32
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
}
+206
View File
@@ -0,0 +1,206 @@
package xiaomi
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/AlexxIT/go2rtc/pkg/xiaomi/miss"
"github.com/pion/rtp"
)
type Producer struct {
core.Connection
client *miss.Client
}
func Dial(rawURL string) (core.Producer, error) {
client, err := miss.Dial(rawURL)
if err != nil {
return nil, err
}
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)
}
medias, err := probe(client, channel, quality)
if err != nil {
_ = client.Close()
return nil, err
}
return &Producer{
Connection: core.Connection{
ID: core.NewID(),
FormatName: "xiaomi",
Protocol: "cs2+udp",
RemoteAddr: client.RemoteAddr().String(),
Source: rawURL,
Medias: medias,
Transport: client,
},
client: client,
}, nil
}
func probe(client *miss.Client, channel, quality uint8) ([]*core.Media, error) {
_ = client.SetDeadline(time.Now().Add(core.ProbeTimeout))
if err := client.VideoStart(channel, quality, 1); err != nil {
return nil, err
}
var video, audio *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 video == nil {
buf := annexb.EncodeToAVCC(pkt.Payload)
if h264.NALUType(buf) == h264.NALUTypeSPS {
video = h264.AVCCToCodec(buf)
}
}
case miss.CodecH265:
if video == nil {
buf := annexb.EncodeToAVCC(pkt.Payload)
if h265.NALUType(buf) == h265.NALUTypeVPS {
video = h265.AVCCToCodec(buf)
}
}
case miss.CodecPCMA:
if audio == nil {
audio = &core.Codec{Name: core.CodecPCMA, ClockRate: 8000}
}
case miss.CodecOPUS:
if audio == nil {
audio = &core.Codec{Name: core.CodecOpus, ClockRate: 48000, Channels: 2}
}
}
if video != nil && audio != nil {
break
}
}
_ = client.SetDeadline(time.Time{})
return []*core.Media{
{
Kind: core.KindVideo,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{video},
},
{
Kind: core.KindAudio,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{audio},
},
{
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{audio.Clone()},
},
}, nil
}
const timestamp40ms = 48000 * 0.040
func (p *Producer) Start() error {
var audioTS uint32
for {
_ = p.client.SetDeadline(time.Now().Add(core.ConnDeadline))
pkt, err := p.client.ReadPacket()
if err != nil {
return err
}
// 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)
}