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,55 @@
package wyze
import (
"fmt"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/tutk"
"github.com/pion/rtp"
)
func (p *Producer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
if err := p.client.StartIntercom(); err != nil {
return fmt.Errorf("wyze: failed to enable intercom: %w", err)
}
// Get the camera's audio codec info (what it sent us = what it accepts)
tutkCodec, sampleRate, channels := p.client.GetBackchannelCodec()
if tutkCodec == 0 {
return fmt.Errorf("wyze: no audio codec detected from camera")
}
if p.client.verbose {
fmt.Printf("[Wyze] Intercom enabled, using codec=0x%04x rate=%d ch=%d\n", tutkCodec, sampleRate, channels)
}
sender := core.NewSender(media, track.Codec)
// Track our own timestamp - camera expects timestamps starting from 0
// and incrementing by frame duration in microseconds
var timestamp uint32 = 0
samplesPerFrame := tutk.GetSamplesPerFrame(tutkCodec)
frameDurationUS := samplesPerFrame * 1000000 / sampleRate
sender.Handler = func(pkt *rtp.Packet) {
if err := p.client.WriteAudio(tutkCodec, pkt.Payload, timestamp, sampleRate, channels); err == nil {
p.Send += len(pkt.Payload)
}
timestamp += frameDurationUS
}
switch track.Codec.Name {
case core.CodecAAC:
if track.Codec.IsRTP() {
sender.Handler = aac.RTPToADTS(codec, sender.Handler)
} else {
sender.Handler = aac.EncodeToADTS(codec, sender.Handler)
}
}
sender.HandleRTP(track)
p.Senders = append(p.Senders, sender)
return nil
}
+618
View File
@@ -0,0 +1,618 @@
package wyze
import (
"crypto/rand"
"encoding/binary"
"encoding/json"
"fmt"
"net"
"net/url"
"strconv"
"strings"
"sync"
"time"
"github.com/AlexxIT/go2rtc/pkg/tutk"
"github.com/AlexxIT/go2rtc/pkg/tutk/dtls"
)
const (
FrameSize1080P = 0
FrameSize360P = 1
FrameSize720P = 2
FrameSize2K = 3
FrameSizeFloodlight = 4
)
const (
BitrateMax uint16 = 0xF0
BitrateSD uint16 = 0x3C
)
const (
MediaTypeVideo = 1
MediaTypeAudio = 2
MediaTypeReturnAudio = 3
MediaTypeRDT = 4
)
const (
KCmdAuth = 10000
KCmdChallenge = 10001
KCmdChallengeResp = 10002
KCmdAuthResult = 10003
KCmdControlChannel = 10010
KCmdControlChannelResp = 10011
KCmdSetResolutionDB = 10052
KCmdSetResolutionDBRes = 10053
KCmdSetResolution = 10056
KCmdSetResolutionResp = 10057
)
type Client struct {
conn *dtls.DTLSConn
host string
uid string
enr string
mac string
model string
authKey string
verbose bool
closed bool
closeMu sync.Mutex
hasAudio bool
hasIntercom bool
audioCodecID byte
audioSampleRate uint32
audioChannels uint8
}
type AuthResponse struct {
ConnectionRes string `json:"connectionRes"`
CameraInfo map[string]any `json:"cameraInfo"`
}
func Dial(rawURL string) (*Client, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, fmt.Errorf("wyze: invalid URL: %w", err)
}
query := u.Query()
if query.Get("dtls") != "true" {
return nil, fmt.Errorf("wyze: only DTLS cameras are supported")
}
c := &Client{
host: u.Host,
uid: query.Get("uid"),
enr: query.Get("enr"),
mac: query.Get("mac"),
model: query.Get("model"),
verbose: query.Get("verbose") == "true",
}
c.authKey = string(dtls.CalculateAuthKey(c.enr, c.mac))
if c.verbose {
fmt.Printf("[Wyze] Connecting to %s (UID: %s)\n", c.host, c.uid)
}
if err := c.connect(); err != nil {
c.Close()
return nil, err
}
if err := c.doAVLogin(); err != nil {
c.Close()
return nil, err
}
if err := c.doKAuth(); err != nil {
c.Close()
return nil, err
}
if c.verbose {
fmt.Printf("[Wyze] Connection established\n")
}
return c, nil
}
func (c *Client) SupportsAudio() bool {
return c.hasAudio
}
func (c *Client) SupportsIntercom() bool {
return c.hasIntercom
}
func (c *Client) SetBackchannelCodec(codecID byte, sampleRate uint32, channels uint8) {
c.audioCodecID = codecID
c.audioSampleRate = sampleRate
c.audioChannels = channels
}
func (c *Client) GetBackchannelCodec() (codecID byte, sampleRate uint32, channels uint8) {
return c.audioCodecID, c.audioSampleRate, c.audioChannels
}
func (c *Client) SetResolution(quality byte) error {
var frameSize uint8
var bitrate uint16
switch quality {
case 0: // Auto/HD - use model's best
frameSize = c.hdFrameSize()
bitrate = BitrateMax
case FrameSize360P: // 1 = SD/360P
frameSize = FrameSize360P
bitrate = BitrateSD
case FrameSize720P: // 2 = 720P
frameSize = FrameSize720P
bitrate = BitrateMax
case FrameSize2K: // 3 = 2K
if c.is2K() {
frameSize = FrameSize2K
} else {
frameSize = c.hdFrameSize()
}
bitrate = BitrateMax
case FrameSizeFloodlight: // 4 = Floodlight
frameSize = c.hdFrameSize()
bitrate = BitrateMax
default:
frameSize = quality
bitrate = BitrateMax
}
if c.verbose {
fmt.Printf("[Wyze] SetResolution: quality=%d frameSize=%d bitrate=%d model=%s\n", quality, frameSize, bitrate, c.model)
}
// Use K10052 (doorbell format) for certain models
if c.useDoorbellResolution() {
k10052 := c.buildK10052(frameSize, bitrate)
_, err := c.conn.WriteAndWaitIOCtrl(k10052, c.matchHL(KCmdSetResolutionDBRes), 5*time.Second)
return err
}
k10056 := c.buildK10056(frameSize, bitrate)
_, err := c.conn.WriteAndWaitIOCtrl(k10056, c.matchHL(KCmdSetResolutionResp), 5*time.Second)
return err
}
func (c *Client) StartVideo() error {
k10010 := c.buildK10010(MediaTypeVideo, true)
_, err := c.conn.WriteAndWaitIOCtrl(k10010, c.matchHL(KCmdControlChannelResp), 5*time.Second)
return err
}
func (c *Client) StartAudio() error {
k10010 := c.buildK10010(MediaTypeAudio, true)
_, err := c.conn.WriteAndWaitIOCtrl(k10010, c.matchHL(KCmdControlChannelResp), 5*time.Second)
return err
}
func (c *Client) StartIntercom() error {
if c.conn == nil {
return fmt.Errorf("connection is nil")
}
if c.conn.IsBackchannelReady() {
return nil
}
k10010 := c.buildK10010(MediaTypeReturnAudio, true)
if _, err := c.conn.WriteAndWaitIOCtrl(k10010, c.matchHL(KCmdControlChannelResp), 5*time.Second); err != nil {
return fmt.Errorf("enable return audio: %w", err)
}
if c.verbose {
fmt.Printf("[Wyze] Speaker channel enabled, waiting for readiness...\n")
}
return c.conn.AVServStart()
}
func (c *Client) StopIntercom() error {
if c.conn == nil || !c.conn.IsBackchannelReady() {
return nil
}
k10010 := c.buildK10010(MediaTypeReturnAudio, false)
c.conn.WriteIOCtrl(k10010)
return c.conn.AVServStop()
}
func (c *Client) ReadPacket() (*tutk.Packet, error) {
return c.conn.AVRecvFrameData()
}
func (c *Client) WriteAudio(codec byte, payload []byte, timestamp uint32, sampleRate uint32, channels uint8) error {
if !c.conn.IsBackchannelReady() {
return fmt.Errorf("speaker channel not connected")
}
if c.verbose {
fmt.Printf("[Wyze] WriteAudio: codec=0x%02x, payload=%d bytes, rate=%d, ch=%d\n", codec, len(payload), sampleRate, channels)
}
return c.conn.AVSendAudioData(codec, payload, timestamp, sampleRate, channels)
}
func (c *Client) SetDeadline(t time.Time) error {
if c.conn != nil {
return c.conn.SetDeadline(t)
}
return nil
}
func (c *Client) Protocol() string {
return "wyze/dtls"
}
func (c *Client) RemoteAddr() net.Addr {
if c.conn != nil {
return c.conn.RemoteAddr()
}
return nil
}
func (c *Client) Close() error {
c.closeMu.Lock()
if c.closed {
c.closeMu.Unlock()
return nil
}
c.closed = true
c.closeMu.Unlock()
if c.verbose {
fmt.Printf("[Wyze] Closing connection\n")
}
c.StopIntercom()
if c.conn != nil {
c.conn.Close()
}
if c.verbose {
fmt.Printf("[Wyze] Connection closed\n")
}
return nil
}
func (c *Client) connect() error {
host := c.host
port := 0
if idx := strings.Index(host, ":"); idx > 0 {
if p, err := strconv.Atoi(host[idx+1:]); err == nil {
port = p
}
host = host[:idx]
}
conn, err := dtls.DialDTLS(host, port, c.uid, c.authKey, c.enr, c.verbose)
if err != nil {
return fmt.Errorf("wyze: connect failed: %w", err)
}
c.conn = conn
if c.verbose {
fmt.Printf("[Wyze] Connected to %s (IOTC + DTLS)\n", conn.RemoteAddr())
}
return nil
}
func (c *Client) doAVLogin() error {
if c.verbose {
fmt.Printf("[Wyze] Sending AV Login\n")
}
if err := c.conn.AVClientStart(5 * time.Second); err != nil {
return fmt.Errorf("wyze: av login failed: %w", err)
}
if c.verbose {
fmt.Printf("[Wyze] AV Login response received\n")
}
return nil
}
func (c *Client) doKAuth() error {
// Step 1: K10000 -> K10001 (Challenge)
data, err := c.conn.WriteAndWaitIOCtrl(c.buildK10000(), c.matchHL(KCmdChallenge), 5*time.Second)
if err != nil {
return fmt.Errorf("wyze: K10001 failed: %w", err)
}
hlData := c.extractHL(data)
challenge, status, err := c.parseK10001(hlData)
if err != nil {
return fmt.Errorf("wyze: K10001 parse failed: %w", err)
}
if c.verbose {
fmt.Printf("[Wyze] K10001 challenge received, status=%d\n", status)
}
// Step 2: K10002 -> K10003 (Auth)
data, err = c.conn.WriteAndWaitIOCtrl(c.buildK10002(challenge, status), c.matchHL(KCmdAuthResult), 5*time.Second)
if err != nil {
return fmt.Errorf("wyze: K10002 failed: %w", err)
}
hlData = c.extractHL(data)
// Parse K10003 response
authResp, err := c.parseK10003(hlData)
if err != nil {
return fmt.Errorf("wyze: K10003 parse failed: %w", err)
}
if c.verbose && authResp != nil {
if jsonBytes, err := json.MarshalIndent(authResp, "", " "); err == nil {
fmt.Printf("[Wyze] K10003 response:\n%s\n", jsonBytes)
}
}
// Extract audio capability from cameraInfo
if authResp != nil && authResp.CameraInfo != nil {
if channelResult, ok := authResp.CameraInfo["channelRequestResult"].(map[string]any); ok {
if audio, ok := channelResult["audio"].(string); ok {
c.hasAudio = audio == "1"
} else {
c.hasAudio = true
}
} else {
c.hasAudio = true
}
} else {
c.hasAudio = true
}
if c.verbose {
fmt.Printf("[Wyze] K10003 auth success\n")
}
c.hasIntercom = c.conn.HasTwoWayStreaming()
if c.verbose {
fmt.Printf("[Wyze] K-auth complete\n")
}
return nil
}
func (c *Client) buildK10000() []byte {
json := []byte(`{"cameraInfo":{"audioEncoderList":[137,138,140]}}`) // 137=PCMU, 138=PCMA, 140=PCM
b := make([]byte, 16+len(json))
copy(b, "HL") // magic
b[2] = 5 // version
binary.LittleEndian.PutUint16(b[4:], KCmdAuth) // 10000
binary.LittleEndian.PutUint16(b[6:], uint16(len(json))) // payload len
copy(b[16:], json)
return b
}
func (c *Client) buildK10002(challenge []byte, status byte) []byte {
resp := generateChallengeResponse(challenge, c.enr, status)
sessionID := make([]byte, 4)
rand.Read(sessionID)
b := make([]byte, 38)
copy(b, "HL") // magic
b[2] = 5 // version
binary.LittleEndian.PutUint16(b[4:], KCmdChallengeResp) // 10002
b[6] = 22 // payload len
copy(b[16:], resp[:16]) // challenge response
copy(b[32:], sessionID) // random session ID
b[36] = 1 // video enabled/disabled
b[37] = 1 // audio enabled/disabled
return b
}
func (c *Client) buildK10010(mediaType byte, enabled bool) []byte {
b := make([]byte, 18)
copy(b, "HL") // magic
b[2] = 5 // version
binary.LittleEndian.PutUint16(b[4:], KCmdControlChannel) // 10010
binary.LittleEndian.PutUint16(b[6:], 2) // payload len
b[16] = mediaType // 1=video, 2=audio, 3=return audio
b[17] = 1 // 1=enable, 2=disable
if !enabled {
b[17] = 2
}
return b
}
func (c *Client) buildK10052(frameSize uint8, bitrate uint16) []byte {
b := make([]byte, 22)
copy(b, "HL") // magic
b[2] = 5 // version
binary.LittleEndian.PutUint16(b[4:], KCmdSetResolutionDB) // 10052
binary.LittleEndian.PutUint16(b[6:], 6) // payload len
binary.LittleEndian.PutUint16(b[16:], bitrate) // bitrate (2 bytes)
b[18] = frameSize + 1 // frame size (1 byte)
// b[19] = fps, b[20:22] = zeros
return b
}
func (c *Client) buildK10056(frameSize uint8, bitrate uint16) []byte {
b := make([]byte, 21)
copy(b, "HL") // magic
b[2] = 5 // version
binary.LittleEndian.PutUint16(b[4:], KCmdSetResolution) // 10056
binary.LittleEndian.PutUint16(b[6:], 5) // payload len
b[16] = frameSize + 1 // frame size
binary.LittleEndian.PutUint16(b[17:], bitrate) // bitrate
// b[19:21] = FPS (0 = auto)
return b
}
func (c *Client) parseK10001(data []byte) (challenge []byte, status byte, err error) {
if c.verbose {
fmt.Printf("[Wyze] parseK10001: received %d bytes\n", len(data))
}
if len(data) < 33 {
return nil, 0, fmt.Errorf("data too short: %d bytes", len(data))
}
if data[0] != 'H' || data[1] != 'L' {
return nil, 0, fmt.Errorf("invalid HL magic: %x %x", data[0], data[1])
}
cmdID := binary.LittleEndian.Uint16(data[4:])
if cmdID != KCmdChallenge {
return nil, 0, fmt.Errorf("expected cmdID 10001, got %d", cmdID)
}
status = data[16]
challenge = make([]byte, 16)
copy(challenge, data[17:33])
return challenge, status, nil
}
func (c *Client) parseK10003(data []byte) (*AuthResponse, error) {
if c.verbose {
fmt.Printf("[Wyze] parseK10003: received %d bytes\n", len(data))
}
if len(data) < 16 {
return &AuthResponse{}, nil
}
if data[0] != 'H' || data[1] != 'L' {
return &AuthResponse{}, nil
}
cmdID := binary.LittleEndian.Uint16(data[4:])
textLen := binary.LittleEndian.Uint16(data[6:])
if cmdID != KCmdAuthResult {
return &AuthResponse{}, nil
}
if len(data) > 16 && textLen > 0 {
jsonData := data[16:]
for i := range jsonData {
if jsonData[i] == '{' {
var resp AuthResponse
if err := json.Unmarshal(jsonData[i:], &resp); err == nil {
if c.verbose {
fmt.Printf("[Wyze] parseK10003: parsed JSON\n")
}
return &resp, nil
}
break
}
}
}
return &AuthResponse{}, nil
}
func (c *Client) useDoorbellResolution() bool {
switch c.model {
case "WYZEDB3", "WVOD1", "HL_WCO2", "WYZEC1":
return true
}
return false
}
func (c *Client) hdFrameSize() uint8 {
if c.isFloodlight() {
return FrameSizeFloodlight
}
if c.is2K() {
return FrameSize2K
}
return FrameSize1080P
}
func (c *Client) is2K() bool {
switch c.model {
case "HL_CAM3P", "HL_PANP", "HL_CAM4", "HL_DB2", "HL_CFL2":
return true
}
return false
}
func (c *Client) isFloodlight() bool {
return c.model == "HL_CFL2"
}
func (c *Client) matchHL(expectCmd uint16) func([]byte) bool {
return func(data []byte) bool {
hlData := c.extractHL(data)
if hlData == nil {
return false
}
cmd, _, ok := tutk.ParseHL(hlData)
return ok && cmd == expectCmd
}
}
func (c *Client) extractHL(data []byte) []byte {
// Try offset 32 (magicIOCtrl, protoVersion)
if hlData := tutk.FindHL(data, 32); hlData != nil {
return hlData
}
// Try offset 36 (magicChannelMsg)
if len(data) >= 36 && data[16] == 0x00 {
return tutk.FindHL(data, 36)
}
return nil
}
const (
statusDefault byte = 1
statusENR16 byte = 3
statusENR32 byte = 6
)
func generateChallengeResponse(challengeBytes []byte, enr string, status byte) []byte {
var secretKey []byte
switch status {
case statusDefault:
secretKey = []byte("FFFFFFFFFFFFFFFF")
case statusENR16:
if len(enr) >= 16 {
secretKey = []byte(enr[:16])
} else {
secretKey = make([]byte, 16)
copy(secretKey, enr)
}
case statusENR32:
if len(enr) >= 16 {
firstKey := []byte(enr[:16])
challengeBytes = tutk.XXTEADecryptVar(challengeBytes, firstKey)
}
if len(enr) >= 32 {
secretKey = []byte(enr[16:32])
} else if len(enr) > 16 {
secretKey = make([]byte, 16)
copy(secretKey, []byte(enr[16:]))
} else {
secretKey = []byte("FFFFFFFFFFFFFFFF")
}
default:
secretKey = []byte("FFFFFFFFFFFFFFFF")
}
return tutk.XXTEADecryptVar(challengeBytes, secretKey)
}
+337
View File
@@ -0,0 +1,337 @@
package wyze
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
)
const (
baseURLAuth = "https://auth-prod.api.wyze.com"
baseURLAPI = "https://api.wyzecam.com"
appName = "com.hualai.WyzeCam"
appVersion = "2.50.0"
)
type Cloud struct {
client *http.Client
apiKey string
keyID string
accessToken string
phoneID string
cameras []*Camera
}
type Camera struct {
MAC string `json:"mac"`
P2PID string `json:"p2p_id"`
ENR string `json:"enr"`
IP string `json:"ip"`
Nickname string `json:"nickname"`
ProductModel string `json:"product_model"`
ProductType string `json:"product_type"`
DTLS int `json:"dtls"`
FirmwareVer string `json:"firmware_ver"`
IsOnline bool `json:"is_online"`
}
type deviceListResponse struct {
Code string `json:"code"`
Msg string `json:"msg"`
Data struct {
DeviceList []deviceInfo `json:"device_list"`
} `json:"data"`
}
type deviceInfo struct {
MAC string `json:"mac"`
ENR string `json:"enr"`
Nickname string `json:"nickname"`
ProductModel string `json:"product_model"`
ProductType string `json:"product_type"`
FirmwareVer string `json:"firmware_ver"`
ConnState int `json:"conn_state"`
DeviceParams deviceParams `json:"device_params"`
}
type deviceParams struct {
P2PID string `json:"p2p_id"`
P2PType int `json:"p2p_type"`
IP string `json:"ip"`
DTLS int `json:"dtls"`
}
type p2pInfoResponse struct {
Code string `json:"code"`
Msg string `json:"msg"`
Data map[string]any `json:"data"`
}
type loginResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
UserID string `json:"user_id"`
MFAOptions []string `json:"mfa_options"`
SMSSessionID string `json:"sms_session_id"`
EmailSessionID string `json:"email_session_id"`
}
func NewCloud(apiKey, keyID string) *Cloud {
return &Cloud{
client: &http.Client{Timeout: 30 * time.Second},
phoneID: generatePhoneID(),
apiKey: apiKey,
keyID: keyID,
}
}
func (c *Cloud) Login(email, password string) error {
payload := map[string]string{
"email": strings.TrimSpace(email),
"password": hashPassword(password),
}
jsonData, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", baseURLAuth+"/api/user/login", strings.NewReader(string(jsonData)))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Apikey", c.apiKey)
req.Header.Set("Keyid", c.keyID)
req.Header.Set("User-Agent", "go2rtc")
resp, err := c.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var errResp apiError
_ = json.Unmarshal(body, &errResp)
if errResp.hasError() {
return fmt.Errorf("wyze: login failed (code %s): %s", errResp.code(), errResp.message())
}
var result loginResponse
if err := json.Unmarshal(body, &result); err != nil {
return fmt.Errorf("wyze: failed to parse login response: %w", err)
}
if len(result.MFAOptions) > 0 {
return &AuthError{
Message: "MFA required",
NeedsMFA: true,
MFAType: strings.Join(result.MFAOptions, ","),
}
}
if result.AccessToken == "" {
return errors.New("wyze: no access token in response")
}
c.accessToken = result.AccessToken
return nil
}
func (c *Cloud) GetCameraList() ([]*Camera, error) {
payload := map[string]any{
"access_token": c.accessToken,
"phone_id": c.phoneID,
"app_name": appName,
"app_ver": appName + "___" + appVersion,
"app_version": appVersion,
"phone_system_type": 1,
"sc": "9f275790cab94a72bd206c8876429f3c",
"sv": "9d74946e652647e9b6c9d59326aef104",
"ts": time.Now().UnixMilli(),
}
jsonData, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", baseURLAPI+"/app/v2/home_page/get_object_list", strings.NewReader(string(jsonData)))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var result deviceListResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("wyze: failed to parse device list: %w", err)
}
if result.Code != "1" {
return nil, fmt.Errorf("wyze: API error: %s - %s", result.Code, result.Msg)
}
c.cameras = nil
for _, dev := range result.Data.DeviceList {
if dev.ProductType != "Camera" {
continue
}
if dev.DeviceParams.IP == "" {
continue // skip cameras without IP (gwell protocol)
}
c.cameras = append(c.cameras, &Camera{
MAC: dev.MAC,
P2PID: dev.DeviceParams.P2PID,
ENR: dev.ENR,
IP: dev.DeviceParams.IP,
Nickname: dev.Nickname,
ProductModel: dev.ProductModel,
ProductType: dev.ProductType,
DTLS: dev.DeviceParams.DTLS,
FirmwareVer: dev.FirmwareVer,
IsOnline: dev.ConnState == 1,
})
}
return c.cameras, nil
}
func (c *Cloud) GetCamera(id string) (*Camera, error) {
if c.cameras == nil {
if _, err := c.GetCameraList(); err != nil {
return nil, err
}
}
id = strings.ToUpper(id)
for _, cam := range c.cameras {
if strings.ToUpper(cam.MAC) == id || strings.EqualFold(cam.Nickname, id) {
return cam, nil
}
}
return nil, fmt.Errorf("wyze: camera not found: %s", id)
}
func (c *Cloud) GetP2PInfo(mac string) (map[string]any, error) {
payload := map[string]any{
"access_token": c.accessToken,
"phone_id": c.phoneID,
"device_mac": mac,
"app_name": appName,
"app_ver": appName + "___" + appVersion,
"app_version": appVersion,
"phone_system_type": 1,
"sc": "9f275790cab94a72bd206c8876429f3c",
"sv": "9d74946e652647e9b6c9d59326aef104",
"ts": time.Now().UnixMilli(),
}
jsonData, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", baseURLAPI+"/app/v2/device/get_iotc_info", strings.NewReader(string(jsonData)))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var result p2pInfoResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
if result.Code != "1" {
return nil, fmt.Errorf("wyze: API error: %s - %s", result.Code, result.Msg)
}
return result.Data, nil
}
type apiError struct {
Code string `json:"code"`
ErrorCode int `json:"errorCode"`
Msg string `json:"msg"`
Description string `json:"description"`
}
func (e *apiError) hasError() bool {
if e.Code == "1" || e.Code == "0" {
return false
}
if e.Code == "" && e.ErrorCode == 0 {
return false
}
return e.Code != "" || e.ErrorCode != 0
}
func (e *apiError) message() string {
if e.Msg != "" {
return e.Msg
}
return e.Description
}
func (e *apiError) code() string {
if e.Code != "" {
return e.Code
}
return fmt.Sprintf("%d", e.ErrorCode)
}
type AuthError struct {
Message string `json:"message"`
NeedsMFA bool `json:"needs_mfa,omitempty"`
MFAType string `json:"mfa_type,omitempty"`
}
func (e *AuthError) Error() string {
return e.Message
}
func generatePhoneID() string {
return core.RandString(16, 16) // 16 hex chars
}
func hashPassword(password string) string {
encoded := strings.TrimSpace(password)
if strings.HasPrefix(strings.ToLower(encoded), "md5:") {
return encoded[4:]
}
for range 3 {
hash := md5.Sum([]byte(encoded))
encoded = hex.EncodeToString(hash[:])
}
return encoded
}
@@ -0,0 +1,277 @@
package wyze
import (
"fmt"
"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"
)
type Producer struct {
core.Connection
client *Client
model string
}
func NewProducer(rawURL string) (*Producer, error) {
client, err := Dial(rawURL)
if err != nil {
return nil, err
}
u, _ := url.Parse(rawURL)
query := u.Query()
// 0 = HD (default), 1 = SD/360P, 2 = 720P, 3 = 2K, 4 = Floodlight
var quality byte
switch s := query.Get("subtype"); s {
case "", "hd":
quality = 0
case "sd":
quality = FrameSize360P
default:
quality = core.ParseByte(s)
}
medias, err := probe(client, quality)
if err != nil {
_ = client.Close()
return nil, err
}
prod := &Producer{
Connection: core.Connection{
ID: core.NewID(),
FormatName: "wyze",
Protocol: client.Protocol(),
RemoteAddr: client.RemoteAddr().String(),
Source: rawURL,
Medias: medias,
Transport: client,
},
client: client,
model: query.Get("model"),
}
return prod, nil
}
func (p *Producer) Start() error {
for {
if p.client.verbose {
fmt.Println("[Wyze] Reading packet...")
}
_ = p.client.SetDeadline(time.Now().Add(core.ConnDeadline))
pkt, err := p.client.ReadPacket()
if err != nil {
return err
}
if pkt == nil {
continue
}
var name string
var pkt2 *core.Packet
switch codecID := pkt.Codec; codecID {
case tutk.CodecH264:
name = core.CodecH264
pkt2 = &core.Packet{
Header: rtp.Header{SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
Payload: annexb.EncodeToAVCC(pkt.Payload),
}
case tutk.CodecH265:
name = core.CodecH265
pkt2 = &core.Packet{
Header: rtp.Header{SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
Payload: annexb.EncodeToAVCC(pkt.Payload),
}
case tutk.CodecPCMU:
name = core.CodecPCMU
pkt2 = &core.Packet{
Header: rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
Payload: pkt.Payload,
}
case tutk.CodecPCMA:
name = core.CodecPCMA
pkt2 = &core.Packet{
Header: rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
Payload: pkt.Payload,
}
case tutk.CodecAACADTS, tutk.CodecAACAlt, tutk.CodecAACRaw, tutk.CodecAACLATM:
name = core.CodecAAC
payload := pkt.Payload
if aac.IsADTS(payload) {
payload = payload[aac.ADTSHeaderLen(payload):]
}
pkt2 = &core.Packet{
Header: rtp.Header{Version: aac.RTPPacketVersionAAC, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
Payload: payload,
}
case tutk.CodecOpus:
name = core.CodecOpus
pkt2 = &core.Packet{
Header: rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
Payload: pkt.Payload,
}
case tutk.CodecPCML:
name = core.CodecPCML
pkt2 = &core.Packet{
Header: rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
Payload: pkt.Payload,
}
case tutk.CodecMP3:
name = core.CodecMP3
pkt2 = &core.Packet{
Header: rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
Payload: pkt.Payload,
}
case tutk.CodecMJPEG:
name = core.CodecJPEG
pkt2 = &core.Packet{
Header: rtp.Header{SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},
Payload: pkt.Payload,
}
default:
continue
}
for _, recv := range p.Receivers {
if recv.Codec.Name == name {
recv.WriteRTP(pkt2)
break
}
}
}
}
func probe(client *Client, quality byte) ([]*core.Media, error) {
client.SetResolution(quality)
client.SetDeadline(time.Now().Add(core.ProbeTimeout))
var vcodec, acodec *core.Codec
var tutkAudioCodec byte
for {
if client.verbose {
fmt.Println("[Wyze] Probing for codecs...")
}
pkt, err := client.ReadPacket()
if err != nil {
return nil, fmt.Errorf("wyze: probe: %w", err)
}
if pkt == nil || len(pkt.Payload) < 5 {
continue
}
switch pkt.Codec {
case tutk.CodecH264:
if vcodec == nil {
buf := annexb.EncodeToAVCC(pkt.Payload)
if len(buf) >= 5 && h264.NALUType(buf) == h264.NALUTypeSPS {
vcodec = h264.AVCCToCodec(buf)
}
}
case tutk.CodecH265:
if vcodec == nil {
buf := annexb.EncodeToAVCC(pkt.Payload)
if len(buf) >= 5 && h265.NALUType(buf) == h265.NALUTypeVPS {
vcodec = h265.AVCCToCodec(buf)
}
}
case tutk.CodecPCMU:
if acodec == nil {
acodec = &core.Codec{Name: core.CodecPCMU, ClockRate: pkt.SampleRate, Channels: pkt.Channels}
tutkAudioCodec = pkt.Codec
}
case tutk.CodecPCMA:
if acodec == nil {
acodec = &core.Codec{Name: core.CodecPCMA, ClockRate: pkt.SampleRate, Channels: pkt.Channels}
tutkAudioCodec = pkt.Codec
}
case tutk.CodecAACAlt, tutk.CodecAACADTS, tutk.CodecAACRaw, tutk.CodecAACLATM:
if acodec == nil {
config := aac.EncodeConfig(aac.TypeAACLC, pkt.SampleRate, pkt.Channels, false)
acodec = aac.ConfigToCodec(config)
tutkAudioCodec = pkt.Codec
}
case tutk.CodecOpus:
if acodec == nil {
acodec = &core.Codec{Name: core.CodecOpus, ClockRate: 48000, Channels: 2}
tutkAudioCodec = pkt.Codec
}
case tutk.CodecPCML:
if acodec == nil {
acodec = &core.Codec{Name: core.CodecPCML, ClockRate: pkt.SampleRate, Channels: pkt.Channels}
tutkAudioCodec = pkt.Codec
}
case tutk.CodecMP3:
if acodec == nil {
acodec = &core.Codec{Name: core.CodecMP3, ClockRate: pkt.SampleRate, Channels: pkt.Channels}
tutkAudioCodec = pkt.Codec
}
case tutk.CodecMJPEG:
if vcodec == nil {
vcodec = &core.Codec{Name: core.CodecJPEG, ClockRate: 90000, PayloadType: core.PayloadTypeRAW}
}
}
if vcodec != nil && (acodec != nil || !client.SupportsAudio()) {
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},
})
if client.SupportsIntercom() {
client.SetBackchannelCodec(tutkAudioCodec, acodec.ClockRate, uint8(acodec.Channels))
medias = append(medias, &core.Media{
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{acodec.Clone()},
})
}
}
if client.verbose {
fmt.Printf("[Wyze] Probed codecs: video=%s audio=%s\n", vcodec.Name, acodec.Name)
if client.SupportsIntercom() {
fmt.Printf("[Wyze] Intercom supported, audio send codec=%s\n", acodec.Name)
}
}
return medias, nil
}