install go2rtc on bob

This commit is contained in:
2026-04-04 19:36:14 +02:00
parent f0b56e63d1
commit ccf88187b8
537 changed files with 69213 additions and 0 deletions
@@ -0,0 +1,64 @@
# Xiaomi Mi Home
[`new in v1.9.13`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.13)
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 supports 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**. 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.
**Features:**
- Multiple Xiaomi accounts supported
- Cameras from multiple regions are supported for a single account
- Two-way audio is supported
- Cameras with multiple lenses are supported
## Setup
1. Go to 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.
**Example**
```yaml
xiaomi:
1234567890: V1:***
streams:
xiaomi1: xiaomi://1234567890:cn@192.168.1.123?did=9876543210&model=isa.camera.hlc7
```
## Configuration
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 quality: `subtype=hd/sd/auto/0-5`.
```yaml
streams:
xiaomi1: xiaomi://***&subtype=sd
```
You can use a second channel for dual cameras: `channel=2`.
```yaml
streams:
xiaomi1: xiaomi://***&channel=2
```
@@ -0,0 +1,362 @@
package xiaomi
import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/xiaomi"
"github.com/AlexxIT/go2rtc/pkg/xiaomi/crypto"
"github.com/rs/zerolog"
)
func Init() {
var v struct {
Cfg map[string]string `yaml:"xiaomi"`
}
app.LoadConfig(&v)
tokens = v.Cfg
log = app.GetLogger("xiaomi")
streams.HandleFunc("xiaomi", func(rawURL string) (core.Producer, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
if u.User != nil {
rawURL, err = getCameraURL(u)
if err != nil {
return nil, err
}
}
log.Debug().Msgf("xiaomi: dial %s", rawURL)
return xiaomi.Dial(rawURL)
})
api.HandleFunc("api/xiaomi", apiXiaomi)
}
var log zerolog.Logger
var tokens map[string]string
var clouds map[string]*xiaomi.Cloud
var cloudsMu sync.Mutex
func getCloud(userID string) (*xiaomi.Cloud, error) {
cloudsMu.Lock()
defer cloudsMu.Unlock()
if cloud := clouds[userID]; cloud != nil {
return cloud, nil
}
cloud := xiaomi.NewCloud(AppXiaomiHome)
if err := cloud.LoginWithToken(userID, tokens[userID]); err != nil {
return nil, err
}
if clouds == nil {
clouds = map[string]*xiaomi.Cloud{userID: cloud}
} else {
clouds[userID] = cloud
}
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) {
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 getLegacyURL(url)
}
return getMissURL(url)
}
func getLegacyURL(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":"TUTK_CS2_MTP"}`,
clientPublic, query.Get("did"),
)
res, err := cloudUserRequest(url.User, "/v2/device/miss_get_vendor", params)
if err != nil {
if strings.Contains(err.Error(), "no available vendor support") {
return getLegacyURL(url)
}
return "", err
}
var v struct {
Vendor struct {
ID byte `json:"vendor"`
Params struct {
UID string `json:"p2p_id"`
} `json:"vendor_params"`
} `json:"vendor"`
PublicKey string `json:"public_key"`
Sign string `json:"sign"`
}
if err = json.Unmarshal(res, &v); err != nil {
return "", err
}
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)
query.Set("vendor", getVendorName(v.Vendor.ID))
if v.Vendor.ID == 1 {
query.Set("uid", v.Vendor.Params.UID)
}
url.RawQuery = query.Encode()
return url.String(), nil
}
func getVendorName(i byte) string {
switch i {
case 1:
return "tutk"
case 3:
return "agora"
case 4:
return "cs2"
case 6:
return "mtp"
}
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":
apiDeviceList(w, r)
case "POST":
apiAuth(w, r)
}
}
func apiDeviceList(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
user := query.Get("id")
if user == "" {
cloudsMu.Lock()
users := make([]string, 0, len(tokens))
for s := range tokens {
users = append(users, s)
}
cloudsMu.Unlock()
api.ResponseJSON(w, users)
return
}
err := func() error {
region := query.Get("region")
res, err := cloudRequest(user, region, "/v2/home/device_list_page", "{}")
if err != nil {
return err
}
var v struct {
List []*Device `json:"list"`
}
log.Trace().Str("user", user).Msgf("[xiaomi] devices list: %s", res)
if err = json.Unmarshal(res, &v); err != nil {
return err
}
var items []*api.Source
for _, device := range v.List {
if !device.HasCamera() {
continue
}
items = append(items, &api.Source{
Name: device.Name,
Info: fmt.Sprintf("ip: %s, mac: %s", device.IP, device.MAC),
URL: fmt.Sprintf("xiaomi://%s:%s@%s?did=%s&model=%s", user, region, device.IP, device.Did, device.Model),
})
}
api.ResponseSources(w, items)
return nil
}()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
type Device struct {
Did string `json:"did"`
Name string `json:"name"`
Model string `json:"model"`
MAC string `json:"mac"`
IP string `json:"localip"`
}
func (d *Device) HasCamera() bool {
return strings.Contains(d.Model, ".camera.") ||
strings.Contains(d.Model, ".cateye.") ||
strings.Contains(d.Model, ".feeder.")
}
var auth *xiaomi.Cloud
func apiAuth(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
username := r.Form.Get("username")
password := r.Form.Get("password")
captcha := r.Form.Get("captcha")
verify := r.Form.Get("verify")
var err error
switch {
case username != "" || password != "":
auth = xiaomi.NewCloud(AppXiaomiHome)
err = auth.Login(username, password)
case captcha != "":
err = auth.LoginWithCaptcha(captcha)
case verify != "":
err = auth.LoginWithVerify(verify)
default:
http.Error(w, "wrong request", http.StatusBadRequest)
return
}
if err == nil {
userID, token := auth.UserToken()
auth = nil
cloudsMu.Lock()
if tokens == nil {
tokens = map[string]string{userID: token}
} else {
tokens[userID] = token
}
cloudsMu.Unlock()
err = app.PatchConfig([]string{"xiaomi", userID}, token)
}
if err != nil {
var login *xiaomi.LoginError
if errors.As(err, &login) {
w.Header().Set("Content-Type", api.MimeJSON)
w.WriteHeader(http.StatusUnauthorized)
_ = json.NewEncoder(w).Encode(err)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
const AppXiaomiHome = "xiaomiio"
func GetBaseURL(region string) string {
switch region {
case "de", "i2", "ru", "sg", "us":
return "https://" + region + ".api.io.mi.com/app"
}
return "https://api.io.mi.com/app"
}