diff --git a/internal/wyze/wyze.go b/internal/wyze/wyze.go index 85d4c19c..d8e53b4d 100644 --- a/internal/wyze/wyze.go +++ b/internal/wyze/wyze.go @@ -188,6 +188,7 @@ func buildStreamURL(cam *wyze.Camera) string { query.Set("uid", cam.P2PID) query.Set("enr", cam.ENR) query.Set("mac", cam.MAC) + query.Set("model", cam.ProductModel) if cam.DTLS == 1 { query.Set("dtls", "true") diff --git a/pkg/wyze/client.go b/pkg/wyze/client.go index ab1394b8..5c531b5e 100644 --- a/pkg/wyze/client.go +++ b/pkg/wyze/client.go @@ -15,10 +15,11 @@ import ( ) const ( - FrameSize1080P = 0 - FrameSize360P = 1 - FrameSize720P = 2 - FrameSize2K = 3 + FrameSize1080P = 0 + FrameSize360P = 1 + FrameSize720P = 2 + FrameSize2K = 3 + FrameSizeFloodlight = 4 ) const ( @@ -51,6 +52,8 @@ const ( KCmdAuthSuccess = 10009 KCmdControlChannel = 10010 KCmdControlChannelResp = 10011 + KCmdSetResolutionDB = 10052 + KCmdSetResolutionDBRes = 10053 KCmdSetResolution = 10056 KCmdSetResolutionResp = 10057 ) @@ -58,10 +61,11 @@ const ( type Client struct { conn *tutk.Conn - host string - uid string - enr string - mac string + host string + uid string + enr string + mac string + model string authKey string verbose bool @@ -99,6 +103,7 @@ func Dial(rawURL string) (*Client, error) { uid: query.Get("uid"), enr: query.Get("enr"), mac: query.Get("mac"), + model: query.Get("model"), verbose: query.Get("verbose") == "true", } @@ -148,20 +153,44 @@ func (c *Client) GetBackchannelCodec() (codecID uint16, sampleRate uint32, chann return c.audioCodecID, c.audioSampleRate, c.audioChannels } -func (c *Client) SetResolution(sd bool) error { +func (c *Client) SetResolution(quality byte) error { var frameSize uint8 var bitrate uint16 - if sd { + switch quality { + case 0: // Auto/HD - use model's best + frameSize = c.hdFrameSize() + bitrate = BitrateMax + case FrameSize360P: // 1 = SD/360P frameSize = FrameSize360P bitrate = BitrateSD - } else { - frameSize = FrameSize2K + 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: sd=%v frameSize=%d bitrate=%d\n", sd, frameSize, bitrate) + 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(KCmdSetResolutionDB, k10052, KCmdSetResolutionDBRes, 5*time.Second) + return err } k10056 := c.buildK10056(frameSize, bitrate) @@ -379,6 +408,18 @@ func (c *Client) buildK10010(mediaType byte, enabled bool) []byte { 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 @@ -493,3 +534,33 @@ func (c *Client) parseK10009(data []byte) (*AuthResponse, error) { 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" +} diff --git a/pkg/wyze/producer.go b/pkg/wyze/producer.go index 7526115f..400002d9 100644 --- a/pkg/wyze/producer.go +++ b/pkg/wyze/producer.go @@ -29,9 +29,18 @@ func NewProducer(rawURL string) (*Producer, error) { u, _ := url.Parse(rawURL) query := u.Query() - sd := query.Get("subtype") == "sd" + // 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, sd) + medias, err := probe(client, quality) if err != nil { _ = client.Close() return nil, err @@ -132,8 +141,8 @@ func (p *Producer) Start() error { } } -func probe(client *Client, sd bool) ([]*core.Media, error) { - _ = client.SetResolution(sd) +func probe(client *Client, quality byte) ([]*core.Media, error) { + _ = client.SetResolution(quality) _ = client.SetDeadline(time.Now().Add(core.ProbeTimeout)) var vcodec, acodec *core.Codec