install go2rtc on bob
This commit is contained in:
@@ -0,0 +1,568 @@
|
||||
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",
|
||||
Body: form,
|
||||
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"]
|
||||
}
|
||||
|
||||
form := url.Values{
|
||||
"_json": {"true"},
|
||||
"icode": {captCode},
|
||||
"retry": {"0"},
|
||||
}
|
||||
|
||||
req = Request{
|
||||
Method: "POST",
|
||||
URL: "https://account.xiaomi.com/identity/auth/send" + name + "Ticket",
|
||||
Body: form,
|
||||
RawCookies: cookies,
|
||||
}.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
|
||||
Body url.Values
|
||||
Headers url.Values
|
||||
RawCookies string
|
||||
}
|
||||
|
||||
func (r Request) Encode() *http.Request {
|
||||
if r.RawParams != "" {
|
||||
r.URL += "?" + r.RawParams
|
||||
}
|
||||
|
||||
var body io.Reader
|
||||
if r.Body != nil {
|
||||
body = strings.NewReader(r.Body.Encode())
|
||||
}
|
||||
|
||||
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.Body != nil {
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
if r.RawCookies != "" {
|
||||
req.Header.Set("Cookie", r.RawCookies)
|
||||
}
|
||||
|
||||
return req
|
||||
}
|
||||
@@ -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,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
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
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 codec
|
||||
// 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)
|
||||
}
|
||||
} 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.CodecAACLATM:
|
||||
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.CodecAACLATM:
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package miss
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/opus"
|
||||
"github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
func (p *Producer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
||||
if err := p.client.StartSpeaker(); 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
|
||||
|
||||
if p.client.SpeakerCodec() == codecPCM {
|
||||
dst := &core.Codec{Name: core.CodecPCML, ClockRate: 8000}
|
||||
transcode := pcm.Transcode(dst, track.Codec)
|
||||
|
||||
sender.Handler = func(pkt *rtp.Packet) {
|
||||
buf = append(buf, transcode(pkt.Payload)...)
|
||||
const size = 2 * 8000 * 0.040 // 16bit 40ms
|
||||
for len(buf) >= size {
|
||||
p.Send += size
|
||||
_ = p.client.WriteAudio(codecPCM, buf[:size])
|
||||
buf = buf[size:]
|
||||
}
|
||||
}
|
||||
} 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(codecPCMA, buf[:size])
|
||||
buf = buf[size:]
|
||||
}
|
||||
}
|
||||
}
|
||||
case core.CodecOpus:
|
||||
if p.client.SpeakerCodec() == codecOPUS {
|
||||
var buf []byte
|
||||
sender.Handler = func(pkt *rtp.Packet) {
|
||||
if buf == nil {
|
||||
buf = pkt.Payload
|
||||
} else {
|
||||
// convert two 20ms to one 40ms
|
||||
buf = opus.JoinFrames(buf, pkt.Payload)
|
||||
p.Send += len(buf)
|
||||
_ = p.client.WriteAudio(codecOPUS, buf)
|
||||
buf = nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sender.Handler = func(pkt *rtp.Packet) {
|
||||
p.Send += len(pkt.Payload)
|
||||
_ = p.client.WriteAudio(codecOPUS, pkt.Payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sender.HandleRTP(track)
|
||||
p.Senders = append(p.Senders, sender)
|
||||
return nil
|
||||
}
|
||||
@@ -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)
|
||||
//}
|
||||
@@ -0,0 +1,506 @@
|
||||
package cs2
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Dial(host, transport string) (*Conn, error) {
|
||||
conn, err := handshake(host, transport)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, isTCP := conn.(*tcpConn)
|
||||
|
||||
c := &Conn{
|
||||
Conn: conn,
|
||||
isTCP: isTCP,
|
||||
channels: [4]*dataChannel{
|
||||
newDataChannel(0, 10), nil, newDataChannel(250, 100), nil,
|
||||
},
|
||||
}
|
||||
go c.worker()
|
||||
return c, nil
|
||||
}
|
||||
|
||||
type Conn struct {
|
||||
net.Conn
|
||||
isTCP bool
|
||||
|
||||
err error
|
||||
seqCh0 uint16
|
||||
seqCh3 uint16
|
||||
|
||||
channels [4]*dataChannel
|
||||
|
||||
cmdMu sync.Mutex
|
||||
cmdAck func()
|
||||
}
|
||||
|
||||
const (
|
||||
magic = 0xF1
|
||||
magicDrw = 0xD1
|
||||
magicTCP = 0x68
|
||||
msgLanSearch = 0x30
|
||||
msgPunchPkt = 0x41
|
||||
msgP2PRdyUDP = 0x42
|
||||
msgP2PRdyTCP = 0x43
|
||||
msgDrw = 0xD0
|
||||
msgDrwAck = 0xD1
|
||||
msgPing = 0xE0
|
||||
msgPong = 0xE1
|
||||
msgClose = 0xF0
|
||||
msgCloseAck = 0xF1
|
||||
)
|
||||
|
||||
func handshake(host, transport string) (net.Conn, error) {
|
||||
conn, err := newUDPConn(host, 32108)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
|
||||
req := []byte{magic, msgLanSearch, 0, 0}
|
||||
res, err := conn.(*udpConn).WriteUntil(req, func(res []byte) bool {
|
||||
return res[1] == msgPunchPkt
|
||||
})
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var msgUDP, msgTCP byte
|
||||
|
||||
if transport == "" || transport == "udp" {
|
||||
msgUDP = msgP2PRdyUDP
|
||||
}
|
||||
if transport == "" || transport == "tcp" {
|
||||
msgTCP = msgP2PRdyTCP
|
||||
}
|
||||
|
||||
res, err = conn.(*udpConn).WriteUntil(res, func(res []byte) bool {
|
||||
return res[1] == msgUDP || res[1] == msgTCP
|
||||
})
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = conn.SetDeadline(time.Time{})
|
||||
|
||||
if res[1] == msgTCP {
|
||||
_ = conn.Close()
|
||||
//host := fmt.Sprintf("%d.%d.%d.%d:%d", b[31], b[30], b[29], b[28], uint16(b[27])<<8|uint16(b[26]))
|
||||
return newTCPConn(conn.RemoteAddr().String())
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (c *Conn) worker() {
|
||||
defer func() {
|
||||
c.channels[0].Close()
|
||||
c.channels[2].Close()
|
||||
}()
|
||||
|
||||
var keepaliveTS time.Time // only for TCP
|
||||
|
||||
buf := make([]byte, 1200)
|
||||
|
||||
for {
|
||||
n, err := c.Conn.Read(buf)
|
||||
if err != nil {
|
||||
c.err = fmt.Errorf("%s: %w", "cs2", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 0 f1d0 magic
|
||||
// 2 005d size = total size + 4
|
||||
// 4 d1 magic
|
||||
// 5 00 channel
|
||||
// 6 0000 seq
|
||||
switch buf[1] {
|
||||
case msgDrw:
|
||||
ch := buf[5]
|
||||
channel := c.channels[ch]
|
||||
|
||||
if c.isTCP {
|
||||
// For TCP we should send ping every second to keep connection alive.
|
||||
// Based on PCAP analysis: official Mi Home app sends PING every ~1s.
|
||||
if now := time.Now(); now.After(keepaliveTS) {
|
||||
_, _ = c.Conn.Write([]byte{magic, msgPing, 0, 0})
|
||||
keepaliveTS = now.Add(time.Second)
|
||||
}
|
||||
|
||||
err = channel.Push(buf[8:n])
|
||||
} else {
|
||||
var pushed int
|
||||
|
||||
seqHI, seqLO := buf[6], buf[7]
|
||||
seq := uint16(seqHI)<<8 | uint16(seqLO)
|
||||
pushed, err = channel.PushSeq(seq, buf[8:n])
|
||||
|
||||
if pushed >= 0 {
|
||||
// For UDP we should send ACK.
|
||||
ack := []byte{magic, msgDrwAck, 0, 6, magicDrw, ch, 0, 1, seqHI, seqLO}
|
||||
_, _ = c.Conn.Write(ack)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.err = fmt.Errorf("%s: %w", "cs2", err)
|
||||
return
|
||||
}
|
||||
|
||||
case msgPing:
|
||||
_, _ = c.Conn.Write([]byte{magic, msgPong, 0, 0})
|
||||
case msgPong, msgP2PRdyUDP, msgP2PRdyTCP, msgClose, msgCloseAck: // skip it
|
||||
case msgDrwAck: // only for UDP
|
||||
if c.cmdAck != nil {
|
||||
c.cmdAck()
|
||||
}
|
||||
default:
|
||||
fmt.Printf("%s: unknown msg: %x\n", "cs2", buf[:n])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) Protocol() string {
|
||||
if c.isTCP {
|
||||
return "cs2+tcp"
|
||||
}
|
||||
return "cs2+udp"
|
||||
}
|
||||
|
||||
func (c *Conn) Version() string {
|
||||
return "CS2"
|
||||
}
|
||||
|
||||
func (c *Conn) Error() error {
|
||||
if c.err != nil {
|
||||
return c.err
|
||||
}
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
func (c *Conn) ReadCommand() (cmd uint32, data []byte, err error) {
|
||||
buf, ok := c.channels[0].Pop()
|
||||
if !ok {
|
||||
return 0, nil, c.Error()
|
||||
}
|
||||
cmd = binary.LittleEndian.Uint32(buf)
|
||||
data = buf[4:]
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) WriteCommand(cmd uint32, data []byte) error {
|
||||
c.cmdMu.Lock()
|
||||
defer c.cmdMu.Unlock()
|
||||
|
||||
req := marshalCmd(0, c.seqCh0, cmd, data)
|
||||
c.seqCh0++
|
||||
|
||||
if c.isTCP {
|
||||
_, err := c.Conn.Write(req)
|
||||
return err
|
||||
}
|
||||
|
||||
var repeat atomic.Int32
|
||||
repeat.Store(5)
|
||||
|
||||
timeout := time.NewTicker(time.Second)
|
||||
defer timeout.Stop()
|
||||
|
||||
c.cmdAck = func() {
|
||||
repeat.Store(0)
|
||||
timeout.Reset(1)
|
||||
}
|
||||
|
||||
for {
|
||||
if _, err := c.Conn.Write(req); 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", "cs2", cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hdrSize = 32
|
||||
|
||||
func (c *Conn) ReadPacket() (hdr, payload []byte, err error) {
|
||||
data, ok := c.channels[2].Pop()
|
||||
if !ok {
|
||||
return nil, nil, c.Error()
|
||||
}
|
||||
return data[:hdrSize], data[hdrSize:], nil
|
||||
}
|
||||
|
||||
func (c *Conn) WritePacket(hdr, payload []byte) error {
|
||||
const offset = 12
|
||||
|
||||
n := hdrSize + uint32(len(payload))
|
||||
req := make([]byte, n+offset)
|
||||
req[0] = magic
|
||||
req[1] = msgDrw
|
||||
binary.BigEndian.PutUint16(req[2:], uint16(n+8))
|
||||
|
||||
req[4] = magicDrw
|
||||
req[5] = 3 // channel
|
||||
binary.BigEndian.PutUint16(req[6:], c.seqCh3)
|
||||
c.seqCh3++
|
||||
binary.BigEndian.PutUint32(req[8:], n)
|
||||
copy(req[offset:], hdr)
|
||||
copy(req[offset+hdrSize:], hdr)
|
||||
|
||||
_, err := c.Conn.Write(req)
|
||||
return err
|
||||
}
|
||||
|
||||
func marshalCmd(channel byte, seq uint16, cmd uint32, payload []byte) []byte {
|
||||
size := len(payload)
|
||||
req := make([]byte, 4+4+4+4+size)
|
||||
|
||||
// 1. message header (4 bytes)
|
||||
req[0] = magic
|
||||
req[1] = msgDrw
|
||||
binary.BigEndian.PutUint16(req[2:], uint16(4+4+4+size))
|
||||
|
||||
// 2. drw? header (4 bytes)
|
||||
req[4] = magicDrw
|
||||
req[5] = channel
|
||||
binary.BigEndian.PutUint16(req[6:], seq)
|
||||
|
||||
// 3. payload size (4 bytes)
|
||||
binary.BigEndian.PutUint32(req[8:], uint32(4+size))
|
||||
|
||||
// 4. payload command (4 bytes)
|
||||
binary.BigEndian.PutUint32(req[12:], cmd)
|
||||
|
||||
// 5. payload
|
||||
copy(req[16:], payload)
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func newUDPConn(host string, port int) (net.Conn, error) {
|
||||
// We using raw net.UDPConn, because RemoteAddr should be changed during handshake.
|
||||
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: port}
|
||||
}
|
||||
|
||||
return &udpConn{UDPConn: conn, addr: addr}, nil
|
||||
}
|
||||
|
||||
type udpConn struct {
|
||||
*net.UDPConn
|
||||
addr *net.UDPAddr
|
||||
}
|
||||
|
||||
func (c *udpConn) Read(b []byte) (n int, err error) {
|
||||
var addr *net.UDPAddr
|
||||
for {
|
||||
n, addr, err = c.UDPConn.ReadFromUDP(b)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if string(addr.IP) == string(c.addr.IP) || n >= 8 {
|
||||
//log.Printf("<- %x", b[:n])
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *udpConn) Write(b []byte) (n int, err error) {
|
||||
//log.Printf("-> %x", b)
|
||||
return c.UDPConn.WriteToUDP(b, c.addr)
|
||||
}
|
||||
|
||||
func (c *udpConn) RemoteAddr() net.Addr {
|
||||
return c.addr
|
||||
}
|
||||
|
||||
func (c *udpConn) WriteUntil(req []byte, ok func(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.UDPConn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if string(addr.IP) != string(c.addr.IP) || n < 16 {
|
||||
continue // skip messages from another IP
|
||||
}
|
||||
|
||||
if ok(buf[:n]) {
|
||||
c.addr.Port = addr.Port
|
||||
return buf[:n], nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newTCPConn(addr string) (net.Conn, error) {
|
||||
conn, err := net.DialTimeout("tcp", addr, 3*time.Second)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tcpConn{conn.(*net.TCPConn), bufio.NewReader(conn)}, nil
|
||||
}
|
||||
|
||||
type tcpConn struct {
|
||||
*net.TCPConn
|
||||
rd *bufio.Reader
|
||||
}
|
||||
|
||||
func (c *tcpConn) Read(p []byte) (n int, err error) {
|
||||
tmp := make([]byte, 8)
|
||||
if _, err = io.ReadFull(c.rd, tmp); err != nil {
|
||||
return
|
||||
}
|
||||
n = int(binary.BigEndian.Uint16(tmp))
|
||||
if len(p) < n {
|
||||
return 0, fmt.Errorf("tcp: buffer too small")
|
||||
}
|
||||
_, err = io.ReadFull(c.rd, p[:n])
|
||||
//log.Printf("<- %x%x", tmp, p[:n])
|
||||
return
|
||||
}
|
||||
|
||||
func (c *tcpConn) Write(req []byte) (n int, err error) {
|
||||
n = len(req)
|
||||
buf := make([]byte, 8+n)
|
||||
binary.BigEndian.PutUint16(buf, uint16(n))
|
||||
buf[2] = magicTCP
|
||||
copy(buf[8:], req)
|
||||
//log.Printf("-> %x", buf)
|
||||
_, err = c.TCPConn.Write(buf)
|
||||
return
|
||||
}
|
||||
|
||||
func newDataChannel(pushSize, popSize int) *dataChannel {
|
||||
c := &dataChannel{}
|
||||
if pushSize > 0 {
|
||||
c.pushBuf = make(map[uint16][]byte, pushSize)
|
||||
c.pushSize = pushSize
|
||||
}
|
||||
if popSize >= 0 {
|
||||
c.popBuf = make(chan []byte, popSize)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
type dataChannel struct {
|
||||
waitSeq uint16
|
||||
pushBuf map[uint16][]byte
|
||||
pushSize int
|
||||
|
||||
waitData []byte
|
||||
waitSize int
|
||||
popBuf chan []byte
|
||||
}
|
||||
|
||||
func (c *dataChannel) Push(b []byte) error {
|
||||
c.waitData = append(c.waitData, b...)
|
||||
|
||||
for len(c.waitData) > 4 {
|
||||
// Every new data starts with size. There can be several data inside one packet.
|
||||
if c.waitSize == 0 {
|
||||
c.waitSize = int(binary.BigEndian.Uint32(c.waitData))
|
||||
c.waitData = c.waitData[4:]
|
||||
}
|
||||
if c.waitSize > len(c.waitData) {
|
||||
break
|
||||
}
|
||||
|
||||
select {
|
||||
case c.popBuf <- c.waitData[:c.waitSize]:
|
||||
default:
|
||||
return fmt.Errorf("pop buffer is full")
|
||||
}
|
||||
|
||||
c.waitData = c.waitData[c.waitSize:]
|
||||
c.waitSize = 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *dataChannel) Pop() ([]byte, bool) {
|
||||
data, ok := <-c.popBuf
|
||||
return data, ok
|
||||
}
|
||||
|
||||
func (c *dataChannel) Close() {
|
||||
close(c.popBuf)
|
||||
}
|
||||
|
||||
// PushSeq returns how many seq were processed.
|
||||
// Returns 0 if seq was saved or processed earlier.
|
||||
// Returns -1 if seq could not be saved (buffer full or disabled).
|
||||
func (c *dataChannel) PushSeq(seq uint16, data []byte) (int, error) {
|
||||
diff := int16(seq - c.waitSeq)
|
||||
// Check if this is seq from the future.
|
||||
if diff > 0 {
|
||||
// Support disabled buffer.
|
||||
if c.pushSize == 0 {
|
||||
return -1, nil // couldn't save seq
|
||||
}
|
||||
// Check if we don't have this seq in the buffer.
|
||||
if c.pushBuf[seq] == nil {
|
||||
// Check if there is enough space in the buffer.
|
||||
if len(c.pushBuf) == c.pushSize {
|
||||
return -1, nil // couldn't save seq
|
||||
}
|
||||
c.pushBuf[seq] = bytes.Clone(data)
|
||||
//log.Printf("push buf wait=%d seq=%d len=%d", c.waitSeq, seq, len(c.pushBuf))
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Check if this is seq from the past.
|
||||
if diff < 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
for i := 1; ; i++ {
|
||||
if err := c.Push(data); err != nil {
|
||||
return i, err
|
||||
}
|
||||
c.waitSeq++
|
||||
// Check if we have next seq in the buffer.
|
||||
if data = c.pushBuf[c.waitSeq]; data != nil {
|
||||
delete(c.pushBuf, c.waitSeq)
|
||||
} else {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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: pkt.SampleRate()}
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package xiaomi
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/xiaomi/legacy"
|
||||
"github.com/AlexxIT/go2rtc/pkg/xiaomi/miss"
|
||||
)
|
||||
|
||||
func Dial(rawURL string) (core.Producer, error) {
|
||||
// Format: xiaomi/miss
|
||||
if strings.Contains(rawURL, "vendor") {
|
||||
return miss.Dial(rawURL)
|
||||
}
|
||||
|
||||
// Format: xiaomi/legacy
|
||||
return legacy.Dial(rawURL)
|
||||
}
|
||||
|
||||
func IsLegacy(model string) bool {
|
||||
return legacy.Supported(model)
|
||||
}
|
||||
Reference in New Issue
Block a user