mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2026-04-22 15:47:06 +08:00
Add support Roborock source
This commit is contained in:
+28
-22
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/roborock"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/rs/zerolog"
|
||||
@@ -26,13 +27,13 @@ func Init() {
|
||||
|
||||
// support load cameras from Hass config file
|
||||
filename := path.Join(conf.Mod.Config, ".storage/core.config_entries")
|
||||
data, err := os.ReadFile(filename)
|
||||
b, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
storage := new(entries)
|
||||
if err = json.Unmarshal(data, storage); err != nil {
|
||||
if err = json.Unmarshal(b, storage); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -48,22 +49,37 @@ func Init() {
|
||||
for _, entrie := range storage.Data.Entries {
|
||||
switch entrie.Domain {
|
||||
case "generic":
|
||||
if entrie.Options.StreamSource == "" {
|
||||
var options struct {
|
||||
StreamSource string `json:"stream_source"`
|
||||
}
|
||||
if err = json.Unmarshal(entrie.Data, &options); err != nil {
|
||||
continue
|
||||
}
|
||||
urls[entrie.Title] = entrie.Options.StreamSource
|
||||
urls[entrie.Title] = options.StreamSource
|
||||
|
||||
case "homekit_controller":
|
||||
if entrie.Data.ClientID == "" {
|
||||
var data struct {
|
||||
ClientID string `json:"iOSPairingId"`
|
||||
ClientPrivate string `json:"iOSDeviceLTSK"`
|
||||
ClientPublic string `json:"iOSDeviceLTPK"`
|
||||
DeviceID string `json:"AccessoryPairingID"`
|
||||
DevicePublic string `json:"AccessoryLTPK"`
|
||||
DeviceHost string `json:"AccessoryIP"`
|
||||
DevicePort uint16 `json:"AccessoryPort"`
|
||||
}
|
||||
if err = json.Unmarshal(entrie.Data, &data); err != nil {
|
||||
continue
|
||||
}
|
||||
urls[entrie.Title] = fmt.Sprintf(
|
||||
"homekit://%s:%d?client_id=%s&client_private=%s%s&device_id=%s&device_public=%s",
|
||||
entrie.Data.DeviceHost, entrie.Data.DevicePort,
|
||||
entrie.Data.ClientID, entrie.Data.ClientPrivate, entrie.Data.ClientPublic,
|
||||
entrie.Data.DeviceID, entrie.Data.DevicePublic,
|
||||
data.DeviceHost, data.DevicePort,
|
||||
data.ClientID, data.ClientPrivate, data.ClientPublic,
|
||||
data.DeviceID, data.DevicePublic,
|
||||
)
|
||||
|
||||
case "roborock":
|
||||
_ = json.Unmarshal(entrie.Data, &roborock.Auth)
|
||||
|
||||
default:
|
||||
continue
|
||||
}
|
||||
@@ -78,20 +94,10 @@ var log zerolog.Logger
|
||||
type entries struct {
|
||||
Data struct {
|
||||
Entries []struct {
|
||||
Title string `json:"title"`
|
||||
Domain string `json:"domain"`
|
||||
Data struct {
|
||||
ClientID string `json:"iOSPairingId"`
|
||||
ClientPrivate string `json:"iOSDeviceLTSK"`
|
||||
ClientPublic string `json:"iOSDeviceLTPK"`
|
||||
DeviceID string `json:"AccessoryPairingID"`
|
||||
DevicePublic string `json:"AccessoryLTPK"`
|
||||
DeviceHost string `json:"AccessoryIP"`
|
||||
DevicePort uint16 `json:"AccessoryPort"`
|
||||
} `json:"data"`
|
||||
Options struct {
|
||||
StreamSource string `json:"stream_source"`
|
||||
}
|
||||
Title string `json:"title"`
|
||||
Domain string `json:"domain"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Options json.RawMessage `json:"options"`
|
||||
} `json:"entries"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package roborock
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/roborock"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("roborock", handle)
|
||||
|
||||
api.HandleFunc("api/roborock", apiHandle)
|
||||
}
|
||||
|
||||
func handle(url string) (core.Producer, error) {
|
||||
conn := roborock.NewClient(url)
|
||||
if err := conn.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := conn.Connect(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
var Auth struct {
|
||||
UserData *roborock.UserInfo `json:"user_data"`
|
||||
BaseURL string `json:"base_url"`
|
||||
}
|
||||
|
||||
func apiHandle(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
if Auth.UserData == nil {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
case "POST":
|
||||
if err := r.ParseMultipartForm(1024); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
username := r.Form.Get("username")
|
||||
password := r.Form.Get("password")
|
||||
if username == "" || password == "" {
|
||||
http.Error(w, "empty username or password", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
base, err := roborock.GetBaseURL(username)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ui, err := roborock.Login(base, username, password)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
Auth.BaseURL = base
|
||||
Auth.UserData = ui
|
||||
|
||||
default:
|
||||
http.Error(w, "", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
homeID, err := roborock.GetHomeID(Auth.BaseURL, Auth.UserData.Token)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
devices, err := roborock.GetDevices(Auth.UserData, homeID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if len(devices) == 0 {
|
||||
http.Error(w, "no devices in the account", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Devices []struct {
|
||||
Name string `json:"name"`
|
||||
Source string `json:"source"`
|
||||
} `json:"devices"`
|
||||
}
|
||||
|
||||
for _, device := range devices {
|
||||
source := fmt.Sprintf(
|
||||
"roborock://%s?u=%s&s=%s&k=%s&did=%s&key=%s&pin=",
|
||||
Auth.UserData.IoT.URL.MQTT[6:],
|
||||
Auth.UserData.IoT.User, Auth.UserData.IoT.Pass, Auth.UserData.IoT.Domain,
|
||||
device.DID, device.Key,
|
||||
)
|
||||
|
||||
response.Devices = append(response.Devices, struct {
|
||||
Name string `json:"name"`
|
||||
Source string `json:"source"`
|
||||
}{
|
||||
Name: device.Name,
|
||||
Source: source,
|
||||
})
|
||||
}
|
||||
|
||||
if err = json.NewEncoder(w).Encode(response); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
@@ -118,8 +118,8 @@ func apiHandle(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// create new random share
|
||||
share = core.RandString(16)
|
||||
pwd := core.RandString(16)
|
||||
share = core.RandString(10, 62)
|
||||
pwd := core.RandString(10, 62)
|
||||
srv.AddShare(share, pwd, src)
|
||||
|
||||
if shares == nil {
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/mp4"
|
||||
"github.com/AlexxIT/go2rtc/cmd/mpegts"
|
||||
"github.com/AlexxIT/go2rtc/cmd/ngrok"
|
||||
"github.com/AlexxIT/go2rtc/cmd/roborock"
|
||||
"github.com/AlexxIT/go2rtc/cmd/rtmp"
|
||||
"github.com/AlexxIT/go2rtc/cmd/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/cmd/srtp"
|
||||
@@ -47,6 +48,7 @@ func main() {
|
||||
tapo.Init()
|
||||
isapi.Init()
|
||||
mpegts.Init()
|
||||
roborock.Init()
|
||||
|
||||
srtp.Init()
|
||||
homekit.Init()
|
||||
|
||||
+4
-4
@@ -8,16 +8,16 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const digits = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
const maxSize = byte(len(digits))
|
||||
const symbols = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"
|
||||
|
||||
func RandString(size byte) string {
|
||||
// RandString base10 - numbers, base16 - hex, base36 - digits+letters, base64 - URL safe symbols
|
||||
func RandString(size, base byte) string {
|
||||
b := make([]byte, size)
|
||||
if _, err := cryptorand.Read(b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for i := byte(0); i < size; i++ {
|
||||
b[i] = digits[b[i]%maxSize]
|
||||
b[i] = symbols[b[i]%base]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package mqtt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
const Timeout = time.Second * 5
|
||||
|
||||
type Client struct {
|
||||
conn net.Conn
|
||||
mid uint16
|
||||
}
|
||||
|
||||
func NewClient(conn net.Conn) *Client {
|
||||
return &Client{conn: conn, mid: 2}
|
||||
}
|
||||
|
||||
func (c *Client) Connect(clientID, username, password string) (err error) {
|
||||
if err = c.conn.SetDeadline(time.Now().Add(Timeout)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
msg := NewConnect(clientID, username, password)
|
||||
if _, err = c.conn.Write(msg.b); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b := make([]byte, 4)
|
||||
if _, err = io.ReadFull(c.conn, b); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !bytes.Equal(b, []byte{CONNACK, 2, 0, 0}) {
|
||||
return errors.New("wrong login")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) Subscribe(topic string) (err error) {
|
||||
if err = c.conn.SetDeadline(time.Now().Add(Timeout)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.mid++
|
||||
msg := NewSubscribe(c.mid, topic, 1)
|
||||
_, err = c.conn.Write(msg.b)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) Publish(topic string, payload []byte) (err error) {
|
||||
if err = c.conn.SetDeadline(time.Now().Add(Timeout)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.mid++
|
||||
msg := NewPublishQOS1(c.mid, topic, payload)
|
||||
_, err = c.conn.Write(msg.b)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) Read() (string, []byte, error) {
|
||||
if err := c.conn.SetDeadline(time.Now().Add(Timeout)); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
b := make([]byte, 1)
|
||||
if _, err := io.ReadFull(c.conn, b); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
size, err := ReadLen(c.conn)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
b0 := b[0]
|
||||
b = make([]byte, size)
|
||||
if _, err = io.ReadFull(c.conn, b); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
if b0&0xF0 != PUBLISH {
|
||||
return "", nil, nil
|
||||
}
|
||||
|
||||
i := binary.BigEndian.Uint16(b)
|
||||
if uint32(i) > size {
|
||||
return "", nil, errors.New("wrong topic size")
|
||||
}
|
||||
|
||||
b = b[2:]
|
||||
|
||||
if qos := (b0 >> 1) & 0b11; qos == 0 {
|
||||
return string(b[:i]), b[i:], nil
|
||||
}
|
||||
|
||||
// response with packet ID
|
||||
_, _ = c.conn.Write([]byte{PUBACK, 2, b[i], b[i+1]})
|
||||
|
||||
return string(b[2:i]), b[i+2:], nil
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
// TODO: Teardown
|
||||
return c.conn.Close()
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package mqtt
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
b []byte
|
||||
}
|
||||
|
||||
// https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html
|
||||
const (
|
||||
CONNECT = 0x10
|
||||
CONNACK = 0x20
|
||||
PUBLISH = 0x30
|
||||
PUBACK = 0x40
|
||||
SUBSCRIBE = 0x82
|
||||
SUBACK = 0x90
|
||||
QOS1 = 0x02
|
||||
)
|
||||
|
||||
func (m *Message) WriteByte(b byte) {
|
||||
m.b = append(m.b, b)
|
||||
}
|
||||
|
||||
func (m *Message) WriteBytes(b []byte) {
|
||||
m.b = append(m.b, b...)
|
||||
}
|
||||
|
||||
func (m *Message) WriteUint16(i uint16) {
|
||||
m.b = append(m.b, byte(i>>8), byte(i))
|
||||
}
|
||||
|
||||
func (m *Message) WriteLen(i int) {
|
||||
for i > 0 {
|
||||
b := byte(i % 128)
|
||||
if i /= 128; i > 0 {
|
||||
b |= 0x80
|
||||
}
|
||||
m.WriteByte(b)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Message) WriteString(s string) {
|
||||
m.WriteUint16(uint16(len(s)))
|
||||
m.b = append(m.b, s...)
|
||||
}
|
||||
|
||||
func (m *Message) Bytes() []byte {
|
||||
return m.b
|
||||
}
|
||||
|
||||
const (
|
||||
flagCleanStart = 0x02
|
||||
flagUsername = 0x80
|
||||
flagPassword = 0x40
|
||||
)
|
||||
|
||||
func NewConnect(clientID, username, password string) *Message {
|
||||
m := &Message{}
|
||||
m.WriteByte(CONNECT)
|
||||
m.WriteLen(16 + len(clientID) + len(username) + len(password))
|
||||
|
||||
m.WriteString("MQTT")
|
||||
m.WriteByte(4) // MQTT version
|
||||
m.WriteByte(flagCleanStart | flagUsername | flagPassword)
|
||||
m.WriteUint16(30) // keepalive
|
||||
|
||||
m.WriteString(clientID)
|
||||
m.WriteString(username)
|
||||
m.WriteString(password)
|
||||
return m
|
||||
}
|
||||
|
||||
func NewSubscribe(mid uint16, topic string, qos byte) *Message {
|
||||
m := &Message{}
|
||||
m.WriteByte(SUBSCRIBE)
|
||||
m.WriteLen(5 + len(topic))
|
||||
|
||||
m.WriteUint16(mid)
|
||||
m.WriteString(topic)
|
||||
m.WriteByte(qos)
|
||||
return m
|
||||
}
|
||||
|
||||
func NewPublish(topic string, payload []byte) *Message {
|
||||
m := &Message{}
|
||||
m.WriteByte(PUBLISH)
|
||||
m.WriteLen(2 + len(topic) + len(payload))
|
||||
|
||||
m.WriteString(topic)
|
||||
m.WriteBytes(payload)
|
||||
return m
|
||||
}
|
||||
|
||||
func NewPublishQOS1(mid uint16, topic string, payload []byte) *Message {
|
||||
m := &Message{}
|
||||
m.WriteByte(PUBLISH | QOS1)
|
||||
m.WriteLen(4 + len(topic) + len(payload))
|
||||
|
||||
m.WriteString(topic)
|
||||
m.WriteUint16(mid)
|
||||
m.WriteBytes(payload)
|
||||
return m
|
||||
}
|
||||
|
||||
func ReadLen(r io.Reader) (uint32, error) {
|
||||
var i uint32
|
||||
var shift byte
|
||||
|
||||
b := []byte{0x80}
|
||||
for b[0]&0x80 != 0 {
|
||||
if _, err := r.Read(b); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
i += uint32(b[0]&0x7F) << shift
|
||||
shift += 7
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package roborock
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserInfo struct {
|
||||
Token string `json:"token"`
|
||||
IoT struct {
|
||||
User string `json:"u"`
|
||||
Pass string `json:"s"`
|
||||
Hash string `json:"h"`
|
||||
Domain string `json:"k"`
|
||||
URL struct {
|
||||
API string `json:"a"`
|
||||
MQTT string `json:"m"`
|
||||
} `json:"r"`
|
||||
} `json:"rriot"`
|
||||
}
|
||||
|
||||
func GetBaseURL(username string) (string, error) {
|
||||
u := "https://euiot.roborock.com/api/v1/getUrlByEmail?email=" + url.QueryEscape(username)
|
||||
req, err := http.NewRequest("POST", u, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
client := http.Client{Timeout: time.Second * 5000}
|
||||
res, err := client.Do(req)
|
||||
|
||||
var v struct {
|
||||
Msg string `json:"msg"`
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err = json.NewDecoder(res.Body).Decode(&v); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if v.Code != 200 {
|
||||
return "", fmt.Errorf("%d: %s", v.Code, v.Msg)
|
||||
}
|
||||
|
||||
return v.Data.URL, nil
|
||||
}
|
||||
|
||||
func Login(baseURL, username, password string) (*UserInfo, error) {
|
||||
u := baseURL + "/api/v1/login?username=" + url.QueryEscape(username) +
|
||||
"&password=" + url.QueryEscape(password) + "&needtwostepauth=false"
|
||||
req, err := http.NewRequest("POST", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clientID := core.RandString(16, 64)
|
||||
clientID = base64.StdEncoding.EncodeToString([]byte(clientID))
|
||||
req.Header.Set("header_clientid", clientID)
|
||||
|
||||
client := http.Client{Timeout: time.Second * 5000}
|
||||
res, err := client.Do(req)
|
||||
|
||||
var v struct {
|
||||
Msg string `json:"msg"`
|
||||
Code int `json:"code"`
|
||||
Data UserInfo `json:"data"`
|
||||
}
|
||||
if err = json.NewDecoder(res.Body).Decode(&v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v.Code != 200 {
|
||||
return nil, fmt.Errorf("%d: %s", v.Code, v.Msg)
|
||||
}
|
||||
|
||||
return &v.Data, nil
|
||||
}
|
||||
|
||||
func GetHomeID(baseURL, token string) (int, error) {
|
||||
req, err := http.NewRequest("GET", baseURL+"/api/v1/getHomeDetail", nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
req.Header.Set("Authorization", token)
|
||||
|
||||
client := http.Client{Timeout: time.Second * 5000}
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var v struct {
|
||||
Msg string `json:"msg"`
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
HomeID int `json:"rrHomeId"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err = json.NewDecoder(res.Body).Decode(&v); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if v.Code != 200 {
|
||||
return 0, fmt.Errorf("%d: %s", v.Code, v.Msg)
|
||||
}
|
||||
|
||||
return v.Data.HomeID, nil
|
||||
}
|
||||
|
||||
type DeviceInfo struct {
|
||||
DID string `json:"duid"`
|
||||
Name string `json:"name"`
|
||||
Key string `json:"localKey"`
|
||||
}
|
||||
|
||||
func GetDevices(ui *UserInfo, homeID int) ([]DeviceInfo, error) {
|
||||
nonce := core.RandString(6, 64)
|
||||
ts := time.Now().Unix()
|
||||
path := "/user/homes/" + strconv.Itoa(homeID)
|
||||
|
||||
mac := fmt.Sprintf(
|
||||
"%s:%s:%s:%d:%x::", ui.IoT.User, ui.IoT.Pass, nonce, ts, md5.Sum([]byte(path)),
|
||||
)
|
||||
hash := hmac.New(sha256.New, []byte(ui.IoT.Hash))
|
||||
hash.Write([]byte(mac))
|
||||
mac = base64.StdEncoding.EncodeToString(hash.Sum(nil))
|
||||
|
||||
auth := fmt.Sprintf(
|
||||
`Hawk id="%s", s="%s", ts="%d", nonce="%s", mac="%s"`,
|
||||
ui.IoT.User, ui.IoT.Pass, ts, nonce, mac,
|
||||
)
|
||||
|
||||
req, err := http.NewRequest("GET", ui.IoT.URL.API+path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", auth)
|
||||
|
||||
client := http.Client{Timeout: time.Second * 5000}
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var v struct {
|
||||
Result struct {
|
||||
Devices []DeviceInfo `json:"devices"`
|
||||
} `json:"result"`
|
||||
}
|
||||
if err = json.NewDecoder(res.Body).Decode(&v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v.Result.Devices, nil
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
package roborock
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/roborock/iot"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
"log"
|
||||
"net/rpc"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
core.Listener
|
||||
|
||||
url string
|
||||
|
||||
conn *webrtc.Conn
|
||||
iot *rpc.Client
|
||||
|
||||
devKey string
|
||||
pin string
|
||||
devTopic string
|
||||
|
||||
audio bool
|
||||
backchannel bool
|
||||
}
|
||||
|
||||
func NewClient(url string) *Client {
|
||||
return &Client{url: url}
|
||||
}
|
||||
|
||||
func (c *Client) Dial() (err error) {
|
||||
u, err := url.Parse(c.url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.iot, err = iot.Dial(c.url)
|
||||
|
||||
c.pin = u.Query().Get("pin")
|
||||
if c.pin != "" {
|
||||
c.pin = fmt.Sprintf("%x", md5.Sum([]byte(c.pin)))
|
||||
return c.CheckHomesecPassword()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Connect() error {
|
||||
// 1. Check if camera ready for connection
|
||||
for i := 0; ; i++ {
|
||||
clientID, err := c.GetHomesecConnectStatus()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if clientID == "none" {
|
||||
break
|
||||
}
|
||||
if err = c.StopCameraPreview(clientID); err != nil {
|
||||
return err
|
||||
}
|
||||
if i == 5 {
|
||||
return errors.New("camera not ready")
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
// 2. Start camera
|
||||
if err := c.StartCameraPreview(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. Get TURN config
|
||||
conf := pion.Configuration{}
|
||||
|
||||
if turn, _ := c.GetTurnServer(); turn != nil {
|
||||
conf.ICEServers = append(conf.ICEServers, *turn)
|
||||
}
|
||||
|
||||
// 4. Create Peer Connection
|
||||
api, err := webrtc.NewAPI("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pc, err := api.NewPeerConnection(conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var connected = make(chan bool)
|
||||
var sendOffer sync.WaitGroup
|
||||
|
||||
c.conn = webrtc.NewConn(pc)
|
||||
c.conn.Desc = "Roborock"
|
||||
c.conn.Mode = core.ModeActiveProducer
|
||||
c.conn.Listen(func(msg any) {
|
||||
switch msg := msg.(type) {
|
||||
case *pion.ICECandidate:
|
||||
if msg != nil && msg.Component == 1 {
|
||||
sendOffer.Wait()
|
||||
_ = c.SendICEtoRobot(msg.ToJSON().Candidate, "0")
|
||||
}
|
||||
case pion.PeerConnectionState:
|
||||
// unblocking write to channel
|
||||
select {
|
||||
case connected <- msg == pion.PeerConnectionStateConnected:
|
||||
default:
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 5. Send Offer
|
||||
sendOffer.Add(1)
|
||||
|
||||
medias := []*core.Media{
|
||||
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
||||
{Kind: core.KindAudio, Direction: core.DirectionSendRecv},
|
||||
}
|
||||
|
||||
if _, err = c.conn.CreateOffer(medias); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("[roborock] offer\n%s", pc.LocalDescription().SDP)
|
||||
if err = c.SendSDPtoRobot(pc.LocalDescription()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sendOffer.Done()
|
||||
|
||||
// 6. Receive answer
|
||||
ts := time.Now().Add(time.Second * 5)
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
|
||||
if desc, _ := c.GetDeviceSDP(); desc != nil {
|
||||
log.Printf("[roborock] answer\n%s", desc.SDP)
|
||||
if err = c.conn.SetAnswer(desc.SDP); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if time.Now().After(ts) {
|
||||
return errors.New("can't get device SDP")
|
||||
}
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(time.Second * 2)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
// 7. Receive remote candidates
|
||||
if pc.ICEConnectionState() == pion.ICEConnectionStateCompleted {
|
||||
ticker.Stop()
|
||||
continue
|
||||
}
|
||||
|
||||
if ice, _ := c.GetDeviceICE(); ice != nil {
|
||||
for _, candidate := range ice {
|
||||
_ = c.conn.AddCandidate(candidate)
|
||||
}
|
||||
}
|
||||
|
||||
case ok := <-connected:
|
||||
// 8. Wait connected result (true or false)
|
||||
if !ok {
|
||||
return errors.New("can't connect")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) CheckHomesecPassword() (err error) {
|
||||
var ok bool
|
||||
|
||||
params := `{"password":"` + c.pin + `"}`
|
||||
if err = c.iot.Call("check_homesec_password", params, &ok); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return errors.New("wrong pin code")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) GetHomesecConnectStatus() (clientID string, err error) {
|
||||
var res []byte
|
||||
|
||||
if err = c.iot.Call("get_homesec_connect_status", nil, &res); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var v struct {
|
||||
Status int `json:"status"`
|
||||
ClientID string `json:"client_id"`
|
||||
}
|
||||
if err = json.Unmarshal(res, &v); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return v.ClientID, nil
|
||||
}
|
||||
|
||||
func (c *Client) StartCameraPreview() error {
|
||||
params := `{"client_id":"676f32727463","quality":"HD","password":"` + c.pin + `"}`
|
||||
return c.Request("start_camera_preview", params)
|
||||
}
|
||||
|
||||
func (c *Client) StopCameraPreview(clientID string) error {
|
||||
params := `{"client_id":"` + clientID + `"}`
|
||||
return c.Request("stop_camera_preview", params)
|
||||
}
|
||||
|
||||
func (c *Client) GetTurnServer() (turn *pion.ICEServer, err error) {
|
||||
var res []byte
|
||||
|
||||
if err = c.iot.Call("get_turn_server", nil, &res); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var v struct {
|
||||
URL string `json:"url"`
|
||||
User string `json:"user"`
|
||||
Pwd string `json:"pwd"`
|
||||
}
|
||||
if err = json.Unmarshal(res, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
turn = &pion.ICEServer{
|
||||
URLs: []string{v.URL},
|
||||
Username: v.User,
|
||||
Credential: v.Pwd,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) SendSDPtoRobot(offer *pion.SessionDescription) (err error) {
|
||||
b, err := json.Marshal(offer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
params := `{"app_sdp":"` + base64.StdEncoding.EncodeToString(b) + `"}`
|
||||
return c.iot.Call("send_sdp_to_robot", params, nil)
|
||||
}
|
||||
|
||||
func (c *Client) SendICEtoRobot(candidate string, mid string) (err error) {
|
||||
b := []byte(`{"candidate":"` + candidate + `","sdpMLineIndex":` + mid + `,"sdpMid":"` + mid + `"}`)
|
||||
|
||||
params := `{"app_ice":"` + base64.StdEncoding.EncodeToString(b) + `"}`
|
||||
return c.iot.Call("send_ice_to_robot", params, nil)
|
||||
}
|
||||
|
||||
func (c *Client) GetDeviceSDP() (sd *pion.SessionDescription, err error) {
|
||||
var res []byte
|
||||
|
||||
if err = c.iot.Call("get_device_sdp", nil, &res); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if string(res) == `{"dev_sdp":"retry"}` {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var v struct {
|
||||
SDP []byte `json:"dev_sdp"`
|
||||
}
|
||||
if err = json.Unmarshal(res, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sd = &pion.SessionDescription{}
|
||||
if err = json.Unmarshal(v.SDP, sd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) GetDeviceICE() (ice []string, err error) {
|
||||
var res []byte
|
||||
|
||||
if err = c.iot.Call("get_device_ice", nil, &res); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if string(res) == `{"dev_ice":"retry"}` {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var v struct {
|
||||
ICE [][]byte `json:"dev_ice"`
|
||||
}
|
||||
if err = json.Unmarshal(res, &v); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, b := range v.ICE {
|
||||
init := pion.ICECandidateInit{}
|
||||
if err = json.Unmarshal(b, &init); err != nil {
|
||||
return
|
||||
}
|
||||
ice = append(ice, init.Candidate)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) StartVoiceChat() error {
|
||||
// record - audio from robot, play - audio to robot?
|
||||
params := fmt.Sprintf(`{"record":%t,"play":%t}`, c.audio, c.backchannel)
|
||||
return c.Request("start_voice_chat", params)
|
||||
}
|
||||
|
||||
func (c *Client) SwitchVideoQuality(hd bool) error {
|
||||
if hd {
|
||||
return c.Request("switch_video_quality", `{"quality":"HD"}`)
|
||||
} else {
|
||||
return c.Request("switch_video_quality", `{"quality":"SD"}`)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) SetVoiceChatVolume(volume int) error {
|
||||
params := `{"volume":` + strconv.Itoa(volume) + `}`
|
||||
return c.Request("set_voice_chat_volume", params)
|
||||
}
|
||||
|
||||
func (c *Client) EnableHomesecVoice(enable bool) error {
|
||||
if enable {
|
||||
return c.Request("enable_homesec_voice", `{"enable":true}`)
|
||||
} else {
|
||||
return c.Request("enable_homesec_voice", `{"enable":false}`)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Request(method string, args any) (err error) {
|
||||
var reply string
|
||||
|
||||
if err = c.iot.Call(method, args, &reply); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if reply != `["ok"]` {
|
||||
return errors.New(reply)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package iot
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mqtt"
|
||||
"github.com/rs/zerolog/log"
|
||||
"net"
|
||||
"net/rpc"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Codec struct {
|
||||
mqtt *mqtt.Client
|
||||
|
||||
devTopic string
|
||||
devKey string
|
||||
|
||||
body json.RawMessage
|
||||
}
|
||||
|
||||
type dps struct {
|
||||
Dps struct {
|
||||
Req string `json:"101,omitempty"`
|
||||
Res string `json:"102,omitempty"`
|
||||
} `json:"dps"`
|
||||
T uint32 `json:"t"`
|
||||
}
|
||||
|
||||
type response struct {
|
||||
ID uint64 `json:"id"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
Error struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
func (c *Codec) WriteRequest(r *rpc.Request, v any) error {
|
||||
if v == nil {
|
||||
v = "[]"
|
||||
}
|
||||
|
||||
ts := uint32(time.Now().Unix())
|
||||
msg := dps{T: ts}
|
||||
msg.Dps.Req = fmt.Sprintf(
|
||||
`{"id":%d,"method":"%s","params":%s}`, r.Seq, r.ServiceMethod, v,
|
||||
)
|
||||
|
||||
payload, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("[roborock] send: %s", payload)
|
||||
|
||||
payload = c.Encrypt(payload, ts, ts, ts)
|
||||
|
||||
return c.mqtt.Publish("rr/m/i/"+c.devTopic, payload)
|
||||
}
|
||||
|
||||
func (c *Codec) ReadResponseHeader(r *rpc.Response) error {
|
||||
for {
|
||||
// receive any message from MQTT
|
||||
_, payload, err := c.mqtt.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// skip if it is not PUBLISH message
|
||||
if payload == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// decrypt MQTT PUBLISH payload
|
||||
if payload, err = c.Decrypt(payload); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// skip if we can't decrypt this payload (ex. binary payload)
|
||||
if payload == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("[roborock] recv %s", payload)
|
||||
|
||||
// get content from response payload:
|
||||
// {"t":1676871268,"dps":{"102":"{\"id\":315003,\"result\":[\"ok\"]}"}}
|
||||
var msg dps
|
||||
if err = json.Unmarshal(payload, &msg); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var res response
|
||||
if err = json.Unmarshal([]byte(msg.Dps.Res), &res); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
r.Seq = res.ID
|
||||
if res.Error.Code != 0 {
|
||||
r.Error = res.Error.Message
|
||||
} else {
|
||||
c.body = res.Result
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Codec) ReadResponseBody(v any) error {
|
||||
switch vv := v.(type) {
|
||||
case *[]byte:
|
||||
*vv = c.body
|
||||
case *string:
|
||||
*vv = string(c.body)
|
||||
case *bool:
|
||||
*vv = string(c.body) == `["ok"]`
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Codec) Close() error {
|
||||
return c.mqtt.Close()
|
||||
}
|
||||
|
||||
func Dial(rawURL string) (*rpc.Client, error) {
|
||||
link, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// dial to MQTT
|
||||
conn, err := net.DialTimeout("tcp", link.Host, time.Second*5)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// process MQTT SSL
|
||||
conf := &tls.Config{ServerName: link.Hostname()}
|
||||
sconn := tls.Client(conn, conf)
|
||||
if err = sconn.Handshake(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := link.Query()
|
||||
|
||||
// send MQTT login
|
||||
uk := md5.Sum([]byte(query.Get("u") + ":" + query.Get("k")))
|
||||
sk := md5.Sum([]byte(query.Get("s") + ":" + query.Get("k")))
|
||||
user := hex.EncodeToString(uk[1:5])
|
||||
pass := hex.EncodeToString(sk[8:])
|
||||
|
||||
c := &Codec{
|
||||
mqtt: mqtt.NewClient(sconn),
|
||||
devKey: query.Get("key"),
|
||||
devTopic: query.Get("u") + "/" + user + "/" + query.Get("did"),
|
||||
}
|
||||
|
||||
if err = c.mqtt.Connect("com.roborock.smart:mbrriot", user, pass); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// subscribe on device topic
|
||||
if err = c.mqtt.Subscribe("rr/m/o/" + c.devTopic); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rpc.NewClientWithCodec(c), nil
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package iot
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/md5"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"hash/crc32"
|
||||
)
|
||||
|
||||
// key - convert timestamp to key
|
||||
func (c *Codec) key(timestamp uint32) []byte {
|
||||
const salt = "TXdfu$jyZ#TZHsg4"
|
||||
key := md5.Sum([]byte(encodeTimestamp(timestamp) + c.devKey + salt))
|
||||
return key[:]
|
||||
}
|
||||
|
||||
func (c *Codec) Decrypt(cipherText []byte) ([]byte, error) {
|
||||
if len(cipherText) < 32 || string(cipherText[:3]) != "1.0" {
|
||||
return nil, errors.New("wrong message prefix")
|
||||
}
|
||||
|
||||
i := len(cipherText) - 4
|
||||
if binary.BigEndian.Uint32(cipherText[i:]) != crc32.ChecksumIEEE(cipherText[:i]) {
|
||||
return nil, errors.New("wrong message checksum")
|
||||
}
|
||||
|
||||
if proto := binary.BigEndian.Uint16(cipherText[15:]); proto != 102 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
timestamp := binary.BigEndian.Uint32(cipherText[11:])
|
||||
return decryptECB(cipherText[19:i], c.key(timestamp)), nil
|
||||
}
|
||||
|
||||
func (c *Codec) Encrypt(plainText []byte, seq, random, timestamp uint32) []byte {
|
||||
const proto = 101
|
||||
|
||||
cipherText := encryptECB(plainText, c.key(timestamp))
|
||||
|
||||
size := uint16(len(cipherText))
|
||||
|
||||
msg := make([]byte, 23+size)
|
||||
copy(msg, "1.0")
|
||||
binary.BigEndian.PutUint32(msg[3:], seq)
|
||||
binary.BigEndian.PutUint32(msg[7:], random)
|
||||
binary.BigEndian.PutUint32(msg[11:], timestamp)
|
||||
binary.BigEndian.PutUint16(msg[15:], proto)
|
||||
binary.BigEndian.PutUint16(msg[17:], size)
|
||||
copy(msg[19:], cipherText)
|
||||
|
||||
crc := crc32.ChecksumIEEE(msg[:19+size])
|
||||
|
||||
binary.BigEndian.PutUint32(msg[19+size:], crc)
|
||||
return msg
|
||||
}
|
||||
|
||||
func encodeTimestamp(i uint32) string {
|
||||
const hextable = "0123456789abcdef"
|
||||
b := []byte{
|
||||
hextable[i>>8&0xF], hextable[i>>4&0xF],
|
||||
hextable[i>>16&0xF], hextable[i&0xF],
|
||||
hextable[i>>24&0xF], hextable[i>>20&0xF],
|
||||
hextable[i>>28&0xF], hextable[i>>12&0xF],
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func pad(plainText []byte, blockSize int) []byte {
|
||||
b0 := byte(blockSize - len(plainText)%blockSize)
|
||||
for i := byte(0); i < b0; i++ {
|
||||
plainText = append(plainText, b0)
|
||||
}
|
||||
return plainText
|
||||
}
|
||||
|
||||
func unpad(paddedText []byte) []byte {
|
||||
padSize := int(paddedText[len(paddedText)-1])
|
||||
return paddedText[:len(paddedText)-padSize]
|
||||
}
|
||||
|
||||
func encryptECB(plainText, key []byte) []byte {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
blockSize := block.BlockSize()
|
||||
plainText = pad(plainText, blockSize)
|
||||
cipherText := plainText
|
||||
|
||||
for len(plainText) > 0 {
|
||||
block.Encrypt(plainText, plainText)
|
||||
plainText = plainText[blockSize:]
|
||||
}
|
||||
|
||||
return cipherText
|
||||
}
|
||||
|
||||
func decryptECB(cipherText, key []byte) []byte {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
blockSize := block.BlockSize()
|
||||
paddedText := cipherText
|
||||
|
||||
for len(cipherText) > 0 {
|
||||
block.Decrypt(cipherText, cipherText)
|
||||
cipherText = cipherText[blockSize:]
|
||||
}
|
||||
|
||||
return unpad(paddedText)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package roborock
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func (c *Client) GetMedias() []*core.Media {
|
||||
return c.conn.GetMedias()
|
||||
}
|
||||
|
||||
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
if media.Kind == core.KindAudio {
|
||||
c.audio = true
|
||||
}
|
||||
|
||||
return c.conn.GetTrack(media, codec)
|
||||
}
|
||||
|
||||
func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||
c.backchannel = true
|
||||
return c.conn.AddTrack(media, codec, track)
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
if c.audio || c.backchannel {
|
||||
if err := c.StartVoiceChat(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.backchannel {
|
||||
if err := c.SetVoiceChatVolume(80); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return c.conn.Start()
|
||||
}
|
||||
|
||||
func (c *Client) Stop() error {
|
||||
_ = c.iot.Close()
|
||||
return c.conn.Stop()
|
||||
}
|
||||
|
||||
func (c *Client) MarshalJSON() ([]byte, error) {
|
||||
return c.conn.MarshalJSON()
|
||||
}
|
||||
@@ -49,7 +49,7 @@ func NewClient(tracker, share, pwd string, pc *pion.PeerConnection) (*webrtc.Con
|
||||
// 5. Send offer
|
||||
msg := fmt.Sprintf(
|
||||
`{"action":"announce","info_hash":"%s","peer_id":"%s","offers":[{"offer_id":"%s","offer":{"type":"offer","sdp":"%s"}}],"numwant":1}`,
|
||||
InfoHash(share), core.RandString(16), nonce, base64.StdEncoding.EncodeToString(enc),
|
||||
InfoHash(share), core.RandString(16, 36), nonce, base64.StdEncoding.EncodeToString(enc),
|
||||
)
|
||||
if err = ws.WriteMessage(websocket.TextMessage, []byte(msg)); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -80,7 +80,7 @@ func (s *Server) Serve() error {
|
||||
continue
|
||||
}
|
||||
|
||||
peerID := core.RandString(16)
|
||||
peerID := core.RandString(16, 36)
|
||||
|
||||
// instant run announce worker
|
||||
s.announce = core.NewWorker(0, func() time.Duration {
|
||||
|
||||
Reference in New Issue
Block a user