Files
go2rtc/internal/homekit/homekit.go
T
Sergey Krashevich c567831c91 feat: add motion detection feature with EMA-based P-frame size analysis
- 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.
2026-03-06 19:58:15 +03:00

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)
}