Files
Strix/internal/camera/discovery/prober_mdns.go
T
eduard256 3fec89be7f Add HTTP prober, optimize mDNS timeout, add Trassir/ZOSI to OUI
- Add HTTPProber: parallel HEAD+GET on ports 80/8080, extracts Server header
- Reduce mDNS timeout from 1s to 100ms using context wrapper around
  mdns.Query (HomeKit devices respond in 2-10ms, no need to wait 1s)
- Add Trassir (F0:23:B9) and ZOSI (00:05:FE) to camera OUI database
- Probe response time improved from ~1s to ~110ms for reachable devices
2026-03-16 14:46:58 +00:00

96 lines
2.7 KiB
Go

package discovery
import (
"context"
"time"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/mdns"
"github.com/eduard256/Strix/internal/models"
)
const (
// mdnsTimeout is the maximum time to wait for mDNS response.
// HomeKit devices respond in 2-10ms. If no response in 100ms,
// the device is definitely not a HomeKit camera.
// The underlying mdns.Query has a 1s internal timeout, but we
// cut it short with this context-based wrapper.
mdnsTimeout = 100 * time.Millisecond
)
// MDNSProber performs mDNS unicast query to detect HomeKit devices.
// It sends a DNS query to ip:5353 for the _hap._tcp.local. service
// and parses TXT records to extract device information.
// Uses a 100ms timeout wrapper around go2rtc's mdns.Query to avoid
// waiting the full 1s on non-HomeKit devices.
type MDNSProber struct{}
func (p *MDNSProber) Name() string { return "mdns" }
// Probe queries the device for HomeKit (HAP) mDNS service.
// Returns nil if the device does not advertise HomeKit or is not a camera/doorbell.
func (p *MDNSProber) Probe(ctx context.Context, ip string) (any, error) {
// Run mdns.Query in a goroutine with 100ms timeout.
// mdns.Query has an internal 1s timeout and doesn't accept context,
// so we wrap it. The background goroutine will clean up on its own
// after the internal timeout expires (~1s, negligible resource cost).
type queryResult struct {
entry *mdns.ServiceEntry
err error
}
ch := make(chan queryResult, 1)
go func() {
entry, err := mdns.Query(ip, mdns.ServiceHAP)
ch <- queryResult{entry, err}
}()
// Wait for result or timeout
timer := time.NewTimer(mdnsTimeout)
defer timer.Stop()
var entry *mdns.ServiceEntry
select {
case r := <-ch:
if r.err != nil || r.entry == nil {
return nil, nil
}
entry = r.entry
case <-timer.C:
return nil, nil // No response within 100ms -- not a HomeKit device
case <-ctx.Done():
return nil, nil
}
// Check if it's complete (has IP, port, and TXT records)
if !entry.Complete() {
return nil, nil
}
// Check if it's a camera or doorbell
category := entry.Info[hap.TXTCategory]
if category != hap.CategoryCamera && category != hap.CategoryDoorbell {
return nil, nil // Not a camera/doorbell, ignore
}
// Map category ID to human-readable name
categoryName := "camera"
if category == hap.CategoryDoorbell {
categoryName = "doorbell"
}
// Determine paired status: sf=0 means paired, sf=1 means not paired
paired := entry.Info[hap.TXTStatusFlags] == hap.StatusPaired
return &models.MDNSProbeResult{
Name: entry.Name,
DeviceID: entry.Info[hap.TXTDeviceID],
Model: entry.Info[hap.TXTModel],
Category: categoryName,
Paired: paired,
Port: int(entry.Port),
Feature: entry.Info[hap.TXTFeatureFlags],
}, nil
}