Add Hass API source for WebRTC cameras
This commit is contained in:
+143
@@ -0,0 +1,143 @@
|
||||
package hass
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/gorilla/websocket"
|
||||
"os"
|
||||
)
|
||||
|
||||
type API struct {
|
||||
ws *websocket.Conn
|
||||
}
|
||||
|
||||
func NewAPI(url, token string) (*API, error) {
|
||||
ws, _, err := websocket.DefaultDialer.Dial(url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api := &API{ws: ws}
|
||||
if err = api.Auth(token); err != nil {
|
||||
_ = ws.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return api, nil
|
||||
}
|
||||
|
||||
func (a *API) Auth(token string) error {
|
||||
var res ResponseAuth
|
||||
|
||||
if err := a.ws.ReadJSON(&res); err != nil {
|
||||
return err
|
||||
}
|
||||
if res.Type != "auth_required" {
|
||||
return errors.New("hass: wrong type: " + res.Type)
|
||||
}
|
||||
|
||||
s := `{"type":"auth","access_token":"` + token + `"}`
|
||||
if err := a.ws.WriteMessage(websocket.TextMessage, []byte(s)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.ws.ReadJSON(&res); err != nil {
|
||||
return err
|
||||
}
|
||||
if res.Type != "auth_ok" {
|
||||
return errors.New("hass: wrong type: " + res.Type)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *API) Close() error {
|
||||
return a.ws.Close()
|
||||
}
|
||||
|
||||
func (a *API) ExchangeSDP(entityID, offer string) (string, error) {
|
||||
var msg = map[string]any{
|
||||
"id": 1,
|
||||
"type": "camera/web_rtc_offer",
|
||||
"entity_id": entityID,
|
||||
"offer": offer,
|
||||
}
|
||||
if err := a.ws.WriteJSON(msg); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var res ResponseOffer
|
||||
if err := a.ws.ReadJSON(&res); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if res.Type != "result" || !res.Success {
|
||||
return "", errors.New("hass: wrong response")
|
||||
}
|
||||
|
||||
return res.Result.Answer, nil
|
||||
}
|
||||
|
||||
func (a *API) GetWebRTCEntities() (map[string]string, error) {
|
||||
s := `{"id":1,"type":"get_states"}`
|
||||
if err := a.ws.WriteMessage(websocket.TextMessage, []byte(s)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res ResponseStates
|
||||
if err := a.ws.ReadJSON(&res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.Type != "result" || !res.Success {
|
||||
return nil, errors.New("hass: wrong response")
|
||||
}
|
||||
|
||||
entities := map[string]string{}
|
||||
|
||||
for _, entity := range res.Result {
|
||||
if entity.Attributes.FrontendStreamType == "web_rtc" {
|
||||
entities[entity.Attributes.FriendlyName] = entity.EntityId
|
||||
}
|
||||
}
|
||||
|
||||
return entities, nil
|
||||
}
|
||||
|
||||
type ResponseAuth struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type ResponseStates struct {
|
||||
//Id int `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Success bool `json:"success"`
|
||||
Result []struct {
|
||||
EntityId string `json:"entity_id"`
|
||||
//State string `json:"state"`
|
||||
Attributes struct {
|
||||
//ModelName string `json:"model_name"`
|
||||
//Brand string `json:"brand"`
|
||||
FrontendStreamType string `json:"frontend_stream_type"`
|
||||
FriendlyName string `json:"friendly_name"`
|
||||
//SupportedFeatures int `json:"supported_features"`
|
||||
} `json:"attributes"`
|
||||
//LastChanged time.Time `json:"last_changed"`
|
||||
//LastUpdated time.Time `json:"last_updated"`
|
||||
//Context struct {
|
||||
// Id string `json:"id"`
|
||||
// ParentId interface{} `json:"parent_id"`
|
||||
// UserId interface{} `json:"user_id"`
|
||||
//} `json:"context"`
|
||||
} `json:"result"`
|
||||
}
|
||||
|
||||
type ResponseOffer struct {
|
||||
//Id int `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Success bool `json:"success"`
|
||||
Result struct {
|
||||
Answer string `json:"answer"`
|
||||
} `json:"result"`
|
||||
}
|
||||
|
||||
func SupervisorToken() string {
|
||||
return os.Getenv("SUPERVISOR_TOKEN")
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package hass
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
conn *webrtc.Conn
|
||||
}
|
||||
|
||||
func NewClient(rawURL string) (*Client, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := u.Query()
|
||||
|
||||
entityID := query.Get("entity_id")
|
||||
if entityID == "" {
|
||||
return nil, errors.New("hass: no entity_id")
|
||||
}
|
||||
|
||||
var uri, token string
|
||||
|
||||
if u.Host == "supervisor" {
|
||||
uri = "ws://supervisor/core/websocket"
|
||||
token = SupervisorToken()
|
||||
} else {
|
||||
uri = "ws://" + u.Host + "/api/websocket"
|
||||
token = query.Get("token")
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
return nil, errors.New("hass: no token")
|
||||
}
|
||||
|
||||
// 1. Check connection to Hass
|
||||
hassAPI, err := NewAPI(uri, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer hassAPI.Close()
|
||||
|
||||
// 2. Create WebRTC client
|
||||
rtcAPI, err := webrtc.NewAPI("")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf := pion.Configuration{}
|
||||
pc, err := rtcAPI.NewPeerConnection(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn := webrtc.NewConn(pc)
|
||||
conn.Desc = "Hass"
|
||||
conn.Mode = core.ModeActiveProducer
|
||||
|
||||
// https://developers.google.com/nest/device-access/traits/device/camera-live-stream#generatewebrtcstream-request-fields
|
||||
medias := []*core.Media{
|
||||
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
|
||||
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
||||
{Kind: "app"}, // important for Nest
|
||||
}
|
||||
|
||||
// 3. Create offer with candidates
|
||||
offer, err := conn.CreateCompleteOffer(medias)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. Exchange SDP via Hass
|
||||
answer, err := hassAPI.ExchangeSDP(entityID, offer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 5. Set answer with remote medias
|
||||
if err = conn.SetAnswer(answer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Client{conn: conn}, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetMedias() []*core.Media {
|
||||
return c.conn.GetMedias()
|
||||
}
|
||||
|
||||
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
return c.conn.GetTrack(media, codec)
|
||||
}
|
||||
|
||||
func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||
return c.conn.AddTrack(media, codec, track)
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
return c.conn.Start()
|
||||
}
|
||||
|
||||
func (c *Client) Stop() error {
|
||||
return c.conn.Stop()
|
||||
}
|
||||
|
||||
func (c *Client) MarshalJSON() ([]byte, error) {
|
||||
return c.conn.MarshalJSON()
|
||||
}
|
||||
@@ -24,6 +24,9 @@ func (c *Conn) CreateOffer(medias []*core.Media) (string, error) {
|
||||
case core.DirectionSendRecv:
|
||||
// default transceiver is sendrecv
|
||||
_, err = c.pc.AddTransceiverFromTrack(NewTrack(media.Kind))
|
||||
default:
|
||||
// Nest cameras require data channel
|
||||
_, err = c.pc.CreateDataChannel(media.Kind, nil)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -148,6 +148,17 @@ func (c *Conn) getTranseiver(mid string) *webrtc.RTPTransceiver {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) getSenderTrack(mid string) *Track {
|
||||
if tr := c.getTranseiver(mid); tr != nil {
|
||||
if s := tr.Sender(); s != nil {
|
||||
if t := s.Track().(*Track); t != nil {
|
||||
return t
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) getMediaCodec(remote *webrtc.TrackRemote) (*core.Media, *core.Codec) {
|
||||
for _, tr := range c.pc.GetTransceivers() {
|
||||
// search Transeiver for this TrackRemote
|
||||
|
||||
@@ -2,6 +2,7 @@ package webrtc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
@@ -31,7 +32,11 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv
|
||||
panic(core.Caller())
|
||||
}
|
||||
|
||||
localTrack := c.getTranseiver(media.ID).Sender().Track().(*Track)
|
||||
localTrack := c.getSenderTrack(media.ID)
|
||||
if localTrack == nil {
|
||||
return errors.New("webrtc: can't get track")
|
||||
}
|
||||
|
||||
payloadType := codec.PayloadType
|
||||
|
||||
sender := core.NewSender(media, codec)
|
||||
|
||||
Reference in New Issue
Block a user