add HLS support and fix skill response

This commit is contained in:
seydx
2025-05-12 05:23:14 +02:00
parent 6e35f1a389
commit b797a2fcd1
2 changed files with 97 additions and 48 deletions
+58 -20
View File
@@ -21,6 +21,7 @@ type TuyaClient struct {
mqtt *TuyaMQTT mqtt *TuyaMQTT
apiURL string apiURL string
rtspURL string rtspURL string
hlsURL string
sessionID string sessionID string
clientID string clientID string
deviceID string deviceID string
@@ -42,11 +43,11 @@ type Token struct {
ExpireTime int64 `json:"expire_time"` ExpireTime int64 `json:"expire_time"`
} }
type RTSPRequest struct { type AllocateRequest struct {
Type string `json:"type"` Type string `json:"type"`
} }
type RTSPResponse struct { type AllocateResponse struct {
Success bool `json:"success"` Success bool `json:"success"`
Result struct { Result struct {
URL string `json:"url"` URL string `json:"url"`
@@ -145,7 +146,7 @@ const (
defaultTimeout = 5 * time.Second 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{ client := &TuyaClient{
httpClient: &http.Client{Timeout: defaultTimeout}, httpClient: &http.Client{Timeout: defaultTimeout},
mqtt: &TuyaMQTT{waiter: core.Waiter{}}, mqtt: &TuyaMQTT{waiter: core.Waiter{}},
@@ -162,9 +163,13 @@ func NewTuyaClient(openAPIURL string, deviceID string, uid string, clientID stri
} }
if useRTSP { 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) 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 { } else {
if err := client.InitDevice(); err != nil { if err := client.InitDevice(); err != nil {
return nil, fmt.Errorf("failed to initialize device: %w", err) return nil, fmt.Errorf("failed to initialize device: %w", err)
@@ -285,8 +290,9 @@ func(c *TuyaClient) InitDevice() (err error) {
} }
c.medias = make([]*core.Media, 0) c.medias = make([]*core.Media, 0)
if len(skill.Audios) > 0 {
for _, audio := range skill.Audios { for _, audio := range skill.Audios {
media := &core.Media{ c.medias = append(c.medias, &core.Media{
Kind: core.KindAudio, Kind: core.KindAudio,
Direction: audioDirection, Direction: audioDirection,
Codecs: []*core.Codec{ Codecs: []*core.Codec{
@@ -296,11 +302,23 @@ func(c *TuyaClient) InitDevice() (err error) {
Channels: uint8(audio.Channels), Channels: uint8(audio.Channels),
}, },
}, },
})
}
} else {
c.medias = append(c.medias, &core.Media{
Kind: core.KindAudio,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{
{
Name: "PCMU",
ClockRate: uint32(8000),
Channels: uint8(1),
},
},
})
} }
c.medias = append(c.medias, media) if len(skill.Videos) > 0 {
}
// take only the first video codec // take only the first video codec
video := skill.Videos[0] video := skill.Videos[0]
@@ -314,7 +332,7 @@ func(c *TuyaClient) InitDevice() (err error) {
name = core.CodecH264 name = core.CodecH264
} }
media := &core.Media{ c.medias = append(c.medias, &core.Media{
Kind: core.KindVideo, Kind: core.KindVideo,
Direction: core.DirectionRecvonly, Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{ Codecs: []*core.Codec{
@@ -324,10 +342,21 @@ func(c *TuyaClient) InitDevice() (err error) {
PayloadType: 96, 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) iceServersBytes, err := json.Marshal(&webRTCConfigResponse.Result.P2PConfig.Ices)
if err != nil { if err != nil {
return fmt.Errorf("failed to marshal ICE servers: %w", err) return fmt.Errorf("failed to marshal ICE servers: %w", err)
@@ -342,11 +371,11 @@ func(c *TuyaClient) InitDevice() (err error) {
return nil 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) url := fmt.Sprintf("https://%s/v1.0/devices/%s/stream/actions/allocate", c.apiURL, c.deviceID)
request := &RTSPRequest{ request := &AllocateRequest{
Type: "rtsp", Type: streamType,
} }
body, err := c.Request("POST", url, request) 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) return fmt.Errorf("failed to get rtsp url: %w", err)
} }
var rtspResponse RTSPResponse var allosResponse AllocateResponse
err = json.Unmarshal(body, &rtspResponse) err = json.Unmarshal(body, &allosResponse)
if err != nil { 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 { if !allosResponse.Success {
return fmt.Errorf("failed to get rtsp url: %s", string(body)) 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 return nil
} }
+13 -2
View File
@@ -41,14 +41,15 @@ func Dial(rawURL string) (core.Producer, error) {
clientID := query.Get("client_id") clientID := query.Get("client_id")
secret := query.Get("secret") secret := query.Get("secret")
resolution := query.Get("resolution") 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 == "" { if deviceID == "" || uid == "" || clientID == "" || secret == "" {
return nil, errors.New("tuya: wrong query") return nil, errors.New("tuya: wrong query")
} }
// Initialize Tuya API client // 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 { if err != nil {
return nil, err return nil, err
} }
@@ -58,6 +59,7 @@ func Dial(rawURL string) (core.Producer, error) {
done: make(chan struct{}), done: make(chan struct{}),
} }
// RTSP
if useRTSP { if useRTSP {
if client.api.rtspURL == "" { if client.api.rtspURL == "" {
return nil, errors.New("tuya: no rtsp url") 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) 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{ conf := pion.Configuration{
ICEServers: client.api.iceServers, ICEServers: client.api.iceServers,
ICETransportPolicy: pion.ICETransportPolicyAll, ICETransportPolicy: pion.ICETransportPolicyAll,