From b797a2fcd18007359917801b27c02f08a61d7fe6 Mon Sep 17 00:00:00 2001 From: seydx Date: Mon, 12 May 2025 05:23:14 +0200 Subject: [PATCH] add HLS support and fix skill response --- pkg/tuya/api.go | 130 +++++++++++++++++++++++++++++---------------- pkg/tuya/client.go | 15 +++++- 2 files changed, 97 insertions(+), 48 deletions(-) diff --git a/pkg/tuya/api.go b/pkg/tuya/api.go index e3c9d5b0..4ba4f1eb 100644 --- a/pkg/tuya/api.go +++ b/pkg/tuya/api.go @@ -21,6 +21,7 @@ type TuyaClient struct { mqtt *TuyaMQTT apiURL string rtspURL string + hlsURL string sessionID string clientID string deviceID string @@ -42,11 +43,11 @@ type Token struct { ExpireTime int64 `json:"expire_time"` } -type RTSPRequest struct { +type AllocateRequest struct { Type string `json:"type"` } -type RTSPResponse struct { +type AllocateResponse struct { Success bool `json:"success"` Result struct { URL string `json:"url"` @@ -145,7 +146,7 @@ const ( defaultTimeout = 5 * time.Second ) -func NewTuyaClient(openAPIURL string, deviceID string, uid string, clientID string, secret string, useRTSP bool) (*TuyaClient, error) { +func NewTuyaClient(openAPIURL string, deviceID string, uid string, clientID string, secret string, useRTSP bool, useHLS bool) (*TuyaClient, error) { client := &TuyaClient{ httpClient: &http.Client{Timeout: defaultTimeout}, mqtt: &TuyaMQTT{waiter: core.Waiter{}}, @@ -162,9 +163,13 @@ func NewTuyaClient(openAPIURL string, deviceID string, uid string, clientID stri } if useRTSP { - if err := client.GetRTSP(); err != nil { + if err := client.GetStreamUrl("rtsp"); err != nil { return nil, fmt.Errorf("failed to get RTSP URL: %w", err) } + } else if useHLS { + if err := client.GetStreamUrl("hls"); err != nil { + return nil, fmt.Errorf("failed to get HLS URL: %w", err) + } } else { if err := client.InitDevice(); err != nil { return nil, fmt.Errorf("failed to initialize device: %w", err) @@ -285,48 +290,72 @@ func(c *TuyaClient) InitDevice() (err error) { } c.medias = make([]*core.Media, 0) - for _, audio := range skill.Audios { - media := &core.Media{ + if len(skill.Audios) > 0 { + for _, audio := range skill.Audios { + c.medias = append(c.medias, &core.Media{ + Kind: core.KindAudio, + Direction: audioDirection, + Codecs: []*core.Codec{ + { + Name: "PCMU", + ClockRate: uint32(audio.SampleRate), + Channels: uint8(audio.Channels), + }, + }, + }) + } + } else { + c.medias = append(c.medias, &core.Media{ Kind: core.KindAudio, - Direction: audioDirection, + Direction: core.DirectionRecvonly, Codecs: []*core.Codec{ { Name: "PCMU", - ClockRate: uint32(audio.SampleRate), - Channels: uint8(audio.Channels), + ClockRate: uint32(8000), + Channels: uint8(1), }, }, + }) + } + + if len(skill.Videos) > 0 { + // take only the first video codec + video := skill.Videos[0] + + var name string + switch video.CodecType { + case 4: + name = core.CodecH265 + case 2: + name = core.CodecH264 + default: + name = core.CodecH264 } - - c.medias = append(c.medias, media) - } - - // take only the first video codec - video := skill.Videos[0] - - var name string - switch video.CodecType { - case 4: - name = core.CodecH265 - case 2: - name = core.CodecH264 - default: - name = core.CodecH264 - } - - media := &core.Media{ - Kind: core.KindVideo, - Direction: core.DirectionRecvonly, - Codecs: []*core.Codec{ - { - Name: name, - ClockRate: uint32(video.SampleRate), - PayloadType: 96, + + c.medias = append(c.medias, &core.Media{ + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + { + Name: name, + ClockRate: uint32(video.SampleRate), + PayloadType: 96, + }, }, - }, + }) + } else { + c.medias = append(c.medias, &core.Media{ + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + { + Name: core.CodecH264, + ClockRate: uint32(90000), + PayloadType: 96, + }, + }, + }) } - - c.medias = append(c.medias, media) iceServersBytes, err := json.Marshal(&webRTCConfigResponse.Result.P2PConfig.Ices) if err != nil { @@ -342,11 +371,11 @@ func(c *TuyaClient) InitDevice() (err error) { return nil } -func(c *TuyaClient) GetRTSP() (err error) { +func(c *TuyaClient) GetStreamUrl(streamType string) (err error) { url := fmt.Sprintf("https://%s/v1.0/devices/%s/stream/actions/allocate", c.apiURL, c.deviceID) - request := &RTSPRequest{ - Type: "rtsp", + request := &AllocateRequest{ + Type: streamType, } body, err := c.Request("POST", url, request) @@ -354,17 +383,26 @@ func(c *TuyaClient) GetRTSP() (err error) { return fmt.Errorf("failed to get rtsp url: %w", err) } - var rtspResponse RTSPResponse - err = json.Unmarshal(body, &rtspResponse) + var allosResponse AllocateResponse + err = json.Unmarshal(body, &allosResponse) if err != nil { - return fmt.Errorf("failed to unmarshal rtsp response: %w", err) + return fmt.Errorf("failed to unmarshal stream response: %w", err) } - if !rtspResponse.Success { - return fmt.Errorf("failed to get rtsp url: %s", string(body)) + if !allosResponse.Success { + return fmt.Errorf("failed to get stream url: %s", string(body)) } - c.rtspURL = rtspResponse.Result.URL + switch streamType { + case "rtsp": + c.rtspURL = allosResponse.Result.URL + fmt.Printf("RTSP URL: %s\n", c.rtspURL) + case "hls": + c.hlsURL = "ffmpeg:" + allosResponse.Result.URL + "#video=copy" + fmt.Printf("HLS URL: %s\n", c.hlsURL) + default: + return fmt.Errorf("unsupported stream type: %s", streamType) + } return nil } diff --git a/pkg/tuya/client.go b/pkg/tuya/client.go index 1f1bc827..9019e931 100644 --- a/pkg/tuya/client.go +++ b/pkg/tuya/client.go @@ -41,14 +41,15 @@ func Dial(rawURL string) (core.Producer, error) { clientID := query.Get("client_id") secret := query.Get("secret") resolution := query.Get("resolution") - useRTSP := query.Get("use_rtsp") == "1" + useRTSP := query.Get("rtsp") == "1" + useHLS := query.Get("hls") == "1" if deviceID == "" || uid == "" || clientID == "" || secret == "" { return nil, errors.New("tuya: wrong query") } // Initialize Tuya API client - tuyaAPI, err := NewTuyaClient(u.Hostname(), deviceID, uid, clientID, secret, useRTSP) + tuyaAPI, err := NewTuyaClient(u.Hostname(), deviceID, uid, clientID, secret, useRTSP, useHLS) if err != nil { return nil, err } @@ -58,6 +59,7 @@ func Dial(rawURL string) (core.Producer, error) { done: make(chan struct{}), } + // RTSP if useRTSP { if client.api.rtspURL == "" { return nil, errors.New("tuya: no rtsp url") @@ -66,6 +68,15 @@ func Dial(rawURL string) (core.Producer, error) { return streams.GetProducer(client.api.rtspURL) } + // HLS + if useHLS { + if client.api.hlsURL == "" { + return nil, errors.New("tuya: no hls url") + } + return streams.GetProducer(client.api.hlsURL) + } + + // Default to WebRTC conf := pion.Configuration{ ICEServers: client.api.iceServers, ICETransportPolicy: pion.ICETransportPolicyAll,