Merge branch 'xiaomi'

# Conflicts:
#	pkg/xiaomi/miss/client.go
#	pkg/xiaomi/miss/cs2/conn.go
#	pkg/xiaomi/producer.go
This commit is contained in:
Alex X
2026-01-17 09:11:25 +03:00
21 changed files with 2390 additions and 906 deletions
+20 -6
View File
@@ -1,11 +1,21 @@
# Xiaomi
**Added in v1.9.13. Improved in v1.9.14.**
This source allows you to view cameras from the [Xiaomi Mi Home](https://home.mi.com/) ecosystem.
Since 2020, Xiaomi has introduced a unified protocol for cameras called `miss`. I think it means **Mi Secure Streaming**. Until this point, the camera protocols were in chaos. Almost every model had different authorization, encryption, command lists, and media packet formats.
Go2rtc support two formats: `xiaomi/mess` and `xiaomi/legacy`.
And multiple P2P protocols: `cs2+udp`, `cs2+tcp`, several versions of `tutk+udp`.
Almost all cameras in the `xiaomi/mess` format and the `cs2` protocol work well.
Older `xiaomi/legacy` format cameras may have support issues.
The `tutk` protocol is the worst thing that's ever happened to the P2P world. It works terribly.
**Important:**
1. **Not all cameras are supported**. There are several P2P protocol vendors in the Xiaomi ecosystem.
Currently, the **CS2** vendor is supported. However, the **TUTK** vendor is not supported.
1. **Not all cameras are supported**. The list of supported cameras is collected in [this issue](https://github.com/AlexxIT/go2rtc/issues/1982).
2. Each time you connect to the camera, you need internet access to obtain encryption keys.
3. Connection to the camera is local only.
@@ -21,7 +31,7 @@ Currently, the **CS2** vendor is supported. However, the **TUTK** vendor is not
1. Goto go2rtc WebUI > Add > Xiaomi > Login with username and password
2. Receive verification code by email or phone if required.
3. Complete the captcha if required.
4. If everything is OK, your account will be added and you can load cameras from it.
4. If everything is OK, your account will be added, and you can load cameras from it.
**Example**
@@ -35,16 +45,20 @@ streams:
## Configuration
You can change camera's quality: `subtype=hd/sd/auto`
Quality in the `miss` protocol is specified by a number from 0 to 5. Usually 0 means auto, 1 - sd, 2 - hd.
Go2rtc by default sets quality to 2. But some new cameras have HD quality at number 3.
Old cameras may have broken codec settings at number 3, so this number should not be set for all cameras.
You can change camera's quality: `subtype=hd/sd/auto/0-5`.
```yaml
streams:
xiaomi1: xiaomi://***&subtype=sd
```
You can use second channel for Dual cameras: `channel=1`
You can use second channel for Dual cameras: `channel=2`.
```yaml
streams:
xiaomi1: xiaomi://***&channel=1
xiaomi1: xiaomi://***&channel=2
```
+88 -19
View File
@@ -15,7 +15,7 @@ import (
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/xiaomi"
"github.com/AlexxIT/go2rtc/pkg/xiaomi/miss"
"github.com/AlexxIT/go2rtc/pkg/xiaomi/crypto"
)
func Init() {
@@ -65,28 +65,96 @@ func getCloud(userID string) (*xiaomi.Cloud, error) {
return cloud, nil
}
func cloudRequest(userID, region, apiURL, params string) ([]byte, error) {
cloud, err := getCloud(userID)
if err != nil {
return nil, err
}
return cloud.Request(GetBaseURL(region), apiURL, params, nil)
}
func cloudUserRequest(user *url.Userinfo, apiURL, params string) ([]byte, error) {
userID := user.Username()
region, _ := user.Password()
return cloudRequest(userID, region, apiURL, params)
}
func getCameraURL(url *url.URL) (string, error) {
clientPublic, clientPrivate, err := miss.GenerateKey()
model := url.Query().Get("model")
// It is not known which models need to be awakened.
// Probably all the doorbells and all the battery cameras.
if strings.Contains(model, ".cateye.") {
_ = wakeUpCamera(url)
}
// The getMissURL request has a fallback to getP2PURL.
// But for known models we can save one request to the cloud.
if xiaomi.IsLegacy(model) {
return getP2PURL(url)
}
return getMissURL(url)
}
func getP2PURL(url *url.URL) (string, error) {
query := url.Query()
clientPublic, clientPrivate, err := crypto.GenerateKey()
if err != nil {
return "", err
}
params := fmt.Sprintf(`{"did":"%s","toSignAppData":"%x"}`, query.Get("did"), clientPublic)
userID := url.User.Username()
region, _ := url.User.Password()
res, err := cloudRequest(userID, region, "/device/devicepass", params)
if err != nil {
return "", err
}
var v struct {
UID string `json:"p2p_id"`
Password string `json:"password"`
PublicKey string `json:"p2p_dev_public_key"`
Sign string `json:"signForAppData"`
}
if err = json.Unmarshal(res, &v); err != nil {
return "", err
}
query.Set("uid", v.UID)
if v.Sign != "" {
query.Set("client_public", hex.EncodeToString(clientPublic))
query.Set("client_private", hex.EncodeToString(clientPrivate))
query.Set("device_public", v.PublicKey)
query.Set("sign", v.Sign)
} else {
query.Set("password", v.Password)
}
url.RawQuery = query.Encode()
return url.String(), nil
}
func getMissURL(url *url.URL) (string, error) {
clientPublic, clientPrivate, err := crypto.GenerateKey()
if err != nil {
return "", err
}
query := url.Query()
params := fmt.Sprintf(
`{"app_pubkey":"%x","did":"%s","support_vendors":"CS2"}`,
`{"app_pubkey":"%x","did":"%s","support_vendors":"TUTK_CS2_MTP"}`,
clientPublic, query.Get("did"),
)
cloud, err := getCloud(url.User.Username())
if err != nil {
return "", err
}
region, _ := url.User.Password()
res, err := cloud.Request(GetBaseURL(region), "/v2/device/miss_get_vendor", params, nil)
res, err := cloudUserRequest(url.User, "/v2/device/miss_get_vendor", params)
if err != nil {
if strings.Contains(err.Error(), "no available vendor support") {
return getP2PURL(url)
}
return "", err
}
@@ -132,6 +200,13 @@ func getVendorName(i byte) string {
return fmt.Sprintf("%d", i)
}
func wakeUpCamera(url *url.URL) error {
const params = `{"id":1,"method":"wakeup","params":{"video":"1"}}`
did := url.Query().Get("did")
_, err := cloudUserRequest(url.User, "/home/rpc/"+did, params)
return err
}
func apiXiaomi(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
@@ -158,14 +233,8 @@ func apiDeviceList(w http.ResponseWriter, r *http.Request) {
}
err := func() error {
cloud, err := getCloud(user)
if err != nil {
return err
}
region := query.Get("region")
res, err := cloud.Request(GetBaseURL(region), "/v2/home/device_list_page", "{}", nil)
res, err := cloudRequest(user, region, "/v2/home/device_list_page", "{}")
if err != nil {
return err
}