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
+144
View File
@@ -0,0 +1,144 @@
package hass
import (
"errors"
"os"
"github.com/gorilla/websocket"
)
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")
}
+118
View File
@@ -0,0 +1,118 @@
package hass
import (
"errors"
"net/url"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v4"
)
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.FormatName = "hass/webrtc"
conn.Mode = core.ModeActiveProducer
conn.Protocol = "ws"
conn.URL = rawURL
// 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()
}