c567831c91
- Implemented MotionDetector for detecting motion based on H.264 P-frame sizes. - Introduced adjustable sensitivity threshold for motion detection. - Added tests for various scenarios including motion detection, hold time, cooldown, and baseline adaptation. - Created hksvSession to manage HDS DataStream connections for HKSV recording. - Updated schema.json to include a new speaker option for 2-way audio support.
362 lines
8.3 KiB
Go
362 lines
8.3 KiB
Go
package homekit
|
|
|
|
import (
|
|
"errors"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/AlexxIT/go2rtc/internal/api"
|
|
"github.com/AlexxIT/go2rtc/internal/app"
|
|
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
|
|
"github.com/AlexxIT/go2rtc/internal/srtp"
|
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
"github.com/AlexxIT/go2rtc/pkg/hap"
|
|
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
|
|
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
|
|
"github.com/AlexxIT/go2rtc/pkg/hksv"
|
|
"github.com/AlexxIT/go2rtc/pkg/homekit"
|
|
"github.com/AlexxIT/go2rtc/pkg/magic"
|
|
"github.com/AlexxIT/go2rtc/pkg/mdns"
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
func Init() {
|
|
var cfg struct {
|
|
Mod map[string]struct {
|
|
Pin string `yaml:"pin"`
|
|
Name string `yaml:"name"`
|
|
DeviceID string `yaml:"device_id"`
|
|
DevicePrivate string `yaml:"device_private"`
|
|
CategoryID string `yaml:"category_id"`
|
|
Pairings []string `yaml:"pairings"`
|
|
HKSV bool `yaml:"hksv"`
|
|
Motion string `yaml:"motion"`
|
|
MotionThreshold float64 `yaml:"motion_threshold"`
|
|
Speaker *bool `yaml:"speaker"`
|
|
} `yaml:"homekit"`
|
|
}
|
|
app.LoadConfig(&cfg)
|
|
|
|
log = app.GetLogger("homekit")
|
|
|
|
streams.HandleFunc("homekit", streamHandler)
|
|
|
|
api.HandleFunc("api/homekit", apiHomekit)
|
|
api.HandleFunc("api/homekit/accessories", apiHomekitAccessories)
|
|
api.HandleFunc("api/homekit/motion", apiMotion)
|
|
api.HandleFunc("api/homekit/doorbell", apiDoorbell)
|
|
api.HandleFunc("api/discovery/homekit", apiDiscovery)
|
|
|
|
if cfg.Mod == nil {
|
|
return
|
|
}
|
|
|
|
hosts = map[string]*hksv.Server{}
|
|
servers = map[string]*hksv.Server{}
|
|
var entries []*mdns.ServiceEntry
|
|
|
|
for id, conf := range cfg.Mod {
|
|
stream := streams.Get(id)
|
|
if stream == nil {
|
|
log.Warn().Msgf("[homekit] missing stream: %s", id)
|
|
continue
|
|
}
|
|
|
|
var proxyURL string
|
|
if url := findHomeKitURL(stream.Sources()); url != "" {
|
|
proxyURL = url
|
|
}
|
|
|
|
srv, err := hksv.NewServer(hksv.Config{
|
|
StreamName: id,
|
|
Pin: conf.Pin,
|
|
Name: conf.Name,
|
|
DeviceID: conf.DeviceID,
|
|
DevicePrivate: conf.DevicePrivate,
|
|
CategoryID: conf.CategoryID,
|
|
Pairings: conf.Pairings,
|
|
ProxyURL: proxyURL,
|
|
HKSV: conf.HKSV,
|
|
MotionMode: conf.Motion,
|
|
MotionThreshold: conf.MotionThreshold,
|
|
Speaker: conf.Speaker,
|
|
UserAgent: app.UserAgent,
|
|
Version: app.Version,
|
|
Streams: &go2rtcStreamProvider{},
|
|
Store: &go2rtcPairingStore{},
|
|
Snapshots: &go2rtcSnapshotProvider{},
|
|
LiveStream: &go2rtcLiveStreamHandler{},
|
|
Logger: log,
|
|
Port: uint16(api.Port),
|
|
})
|
|
if err != nil {
|
|
log.Error().Err(err).Str("stream", id).Msg("[homekit] create server failed")
|
|
continue
|
|
}
|
|
|
|
entry := srv.MDNSEntry()
|
|
entries = append(entries, entry)
|
|
|
|
host := entry.Host(mdns.ServiceHAP)
|
|
hosts[host] = srv
|
|
servers[id] = srv
|
|
|
|
log.Trace().Msgf("[homekit] new server: %s", entry)
|
|
}
|
|
|
|
api.HandleFunc(hap.PathPairSetup, hapHandler)
|
|
api.HandleFunc(hap.PathPairVerify, hapHandler)
|
|
|
|
go func() {
|
|
if err := mdns.Serve(mdns.ServiceHAP, entries); err != nil {
|
|
log.Error().Err(err).Caller().Send()
|
|
}
|
|
}()
|
|
}
|
|
|
|
var log zerolog.Logger
|
|
var hosts map[string]*hksv.Server
|
|
var servers map[string]*hksv.Server
|
|
|
|
// go2rtcStreamProvider implements hksv.StreamProvider
|
|
type go2rtcStreamProvider struct{}
|
|
|
|
func (p *go2rtcStreamProvider) AddConsumer(name string, cons core.Consumer) error {
|
|
stream := streams.Get(name)
|
|
if stream == nil {
|
|
return errors.New("stream not found: " + name)
|
|
}
|
|
return stream.AddConsumer(cons)
|
|
}
|
|
|
|
func (p *go2rtcStreamProvider) RemoveConsumer(name string, cons core.Consumer) {
|
|
if s := streams.Get(name); s != nil {
|
|
s.RemoveConsumer(cons)
|
|
}
|
|
}
|
|
|
|
// go2rtcPairingStore implements hksv.PairingStore
|
|
type go2rtcPairingStore struct{}
|
|
|
|
func (s *go2rtcPairingStore) SavePairings(name string, pairings []string) error {
|
|
return app.PatchConfig([]string{"homekit", name, "pairings"}, pairings)
|
|
}
|
|
|
|
// go2rtcSnapshotProvider implements hksv.SnapshotProvider
|
|
type go2rtcSnapshotProvider struct{}
|
|
|
|
func (s *go2rtcSnapshotProvider) GetSnapshot(streamName string, width, height int) ([]byte, error) {
|
|
stream := streams.Get(streamName)
|
|
if stream == nil {
|
|
return nil, errors.New("stream not found: " + streamName)
|
|
}
|
|
|
|
cons := magic.NewKeyframe()
|
|
if err := stream.AddConsumer(cons); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
once := &core.OnceBuffer{}
|
|
_, _ = cons.WriteTo(once)
|
|
b := once.Buffer()
|
|
|
|
stream.RemoveConsumer(cons)
|
|
|
|
switch cons.CodecName() {
|
|
case core.CodecH264, core.CodecH265:
|
|
var err error
|
|
if b, err = ffmpeg.JPEGWithScale(b, width, height); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
// go2rtcLiveStreamHandler implements hksv.LiveStreamHandler
|
|
type go2rtcLiveStreamHandler struct {
|
|
mu sync.Mutex
|
|
consumer *homekit.Consumer
|
|
}
|
|
|
|
func (h *go2rtcLiveStreamHandler) SetupEndpoints(conn net.Conn, offer *camera.SetupEndpointsRequest) (any, error) {
|
|
consumer := homekit.NewConsumer(conn, srtp.Server)
|
|
consumer.SetOffer(offer)
|
|
|
|
h.mu.Lock()
|
|
h.consumer = consumer
|
|
h.mu.Unlock()
|
|
|
|
answer := consumer.GetAnswer()
|
|
v, err := tlv8.MarshalBase64(answer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
func (h *go2rtcLiveStreamHandler) GetEndpointsResponse() any {
|
|
h.mu.Lock()
|
|
consumer := h.consumer
|
|
h.mu.Unlock()
|
|
if consumer == nil {
|
|
return nil
|
|
}
|
|
answer := consumer.GetAnswer()
|
|
v, _ := tlv8.MarshalBase64(answer)
|
|
return v
|
|
}
|
|
|
|
func (h *go2rtcLiveStreamHandler) StartStream(streamName string, conf *camera.SelectedStreamConfiguration, connTracker hksv.ConnTracker) error {
|
|
h.mu.Lock()
|
|
consumer := h.consumer
|
|
h.mu.Unlock()
|
|
|
|
if consumer == nil {
|
|
return errors.New("no consumer")
|
|
}
|
|
|
|
if !consumer.SetConfig(conf) {
|
|
return errors.New("wrong config")
|
|
}
|
|
|
|
connTracker.AddConn(consumer)
|
|
|
|
stream := streams.Get(streamName)
|
|
if err := stream.AddConsumer(consumer); err != nil {
|
|
return err
|
|
}
|
|
|
|
go func() {
|
|
_, _ = consumer.WriteTo(nil)
|
|
stream.RemoveConsumer(consumer)
|
|
connTracker.DelConn(consumer)
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (h *go2rtcLiveStreamHandler) StopStream(sessionID string, connTracker hksv.ConnTracker) error {
|
|
h.mu.Lock()
|
|
consumer := h.consumer
|
|
h.mu.Unlock()
|
|
|
|
if consumer != nil && consumer.SessionID() == sessionID {
|
|
_ = consumer.Stop()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func streamHandler(rawURL string) (core.Producer, error) {
|
|
if srtp.Server == nil {
|
|
return nil, errors.New("homekit: can't work without SRTP server")
|
|
}
|
|
|
|
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
|
|
client, err := homekit.Dial(rawURL, srtp.Server)
|
|
if client != nil && rawQuery != "" {
|
|
query := streams.ParseQuery(rawQuery)
|
|
client.MaxWidth = core.Atoi(query.Get("maxwidth"))
|
|
client.MaxHeight = core.Atoi(query.Get("maxheight"))
|
|
client.Bitrate = parseBitrate(query.Get("bitrate"))
|
|
}
|
|
|
|
return client, err
|
|
}
|
|
|
|
func resolve(host string) *hksv.Server {
|
|
if len(hosts) == 1 {
|
|
for _, srv := range hosts {
|
|
return srv
|
|
}
|
|
}
|
|
if srv, ok := hosts[host]; ok {
|
|
return srv
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func hapHandler(w http.ResponseWriter, r *http.Request) {
|
|
srv := resolve(r.Host)
|
|
if srv == nil {
|
|
log.Error().Msg("[homekit] unknown host: " + r.Host)
|
|
return
|
|
}
|
|
srv.Handle(w, r)
|
|
}
|
|
|
|
func findHomeKitURL(sources []string) string {
|
|
if len(sources) == 0 {
|
|
return ""
|
|
}
|
|
|
|
url := sources[0]
|
|
if strings.HasPrefix(url, "homekit") {
|
|
return url
|
|
}
|
|
|
|
if strings.HasPrefix(url, "hass") {
|
|
location, _ := streams.Location(url)
|
|
if strings.HasPrefix(location, "homekit") {
|
|
return location
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func apiMotion(w http.ResponseWriter, r *http.Request) {
|
|
id := r.URL.Query().Get("id")
|
|
srv := servers[id]
|
|
if srv == nil {
|
|
http.Error(w, "server not found: "+id, http.StatusNotFound)
|
|
return
|
|
}
|
|
switch r.Method {
|
|
case "POST":
|
|
srv.SetMotionDetected(true)
|
|
case "DELETE":
|
|
srv.SetMotionDetected(false)
|
|
default:
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func apiDoorbell(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
id := r.URL.Query().Get("id")
|
|
srv := servers[id]
|
|
if srv == nil {
|
|
http.Error(w, "server not found: "+id, http.StatusNotFound)
|
|
return
|
|
}
|
|
srv.TriggerDoorbell()
|
|
}
|
|
|
|
func parseBitrate(s string) int {
|
|
n := len(s)
|
|
if n == 0 {
|
|
return 0
|
|
}
|
|
|
|
var k int
|
|
switch n--; s[n] {
|
|
case 'K':
|
|
k = 1024
|
|
s = s[:n]
|
|
case 'M':
|
|
k = 1024 * 1024
|
|
s = s[:n]
|
|
default:
|
|
k = 1
|
|
}
|
|
|
|
return k * core.Atoi(s)
|
|
}
|