20 KiB
hksv - HomeKit Secure Video Library for Go
hksv is a standalone Go library that implements HomeKit Secure Video (HKSV) recording, motion detection, and HAP (HomeKit Accessory Protocol) camera server functionality. It can be used independently of go2rtc in any Go project that needs HKSV support.
Author
Sergei "svk" Krashevich svk@svk.su
Features
- HKSV Recording - Fragmented MP4 (fMP4) muxing with GOP-based buffering, sent over HDS (HomeKit DataStream)
- Motion Detection - P-frame size analysis using EMA (Exponential Moving Average) baseline with configurable threshold
- HAP Server - Full HomeKit pairing (SRP), encrypted communication, accessory management
- Proxy Mode - Transparent proxy for existing HomeKit cameras
- Live Streaming - Pluggable interface for RTP/SRTP live view (bring your own implementation)
- Zero internal dependencies - Only depends on
pkg/packages, never oninternal/
Architecture
pkg/hksv/
hksv.go - Server, Config, interfaces (StreamProvider, PairingStore, etc.)
consumer.go - HKSVConsumer: fMP4 muxer + GOP buffer + HDS sender
session.go - hksvSession: HDS DataStream lifecycle management
motion.go - MotionDetector: P-frame based motion detection
helpers.go - Helper functions for ID/name generation
consumer_test.go - Consumer tests and benchmarks
motion_test.go - Motion detector tests and benchmarks
Dependency Graph
pkg/hksv/
-> pkg/core (Consumer, Connection, Media, Codec, Receiver, Sender)
-> pkg/hap (Server, Conn, Accessory, Character)
-> pkg/hap/hds (Conn, Session - encrypted DataStream)
-> pkg/hap/camera (TLV8 structs, services, accessory factories)
-> pkg/hap/tlv8 (marshal/unmarshal)
-> pkg/homekit (ServerHandler, ProxyHandler, HandlerFunc)
-> pkg/mp4 (Muxer - fMP4)
-> pkg/h264 (IsKeyframe, RTPDepay, RepairAVCC)
-> pkg/aac (RTPDepay)
-> pkg/mdns (ServiceEntry for mDNS advertisement)
-> github.com/pion/rtp
-> github.com/rs/zerolog
-> ZERO imports from internal/
Quick Start
Minimal HKSV Camera
package main
import (
"net/http"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/hksv"
"github.com/AlexxIT/go2rtc/pkg/mdns"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
logger := log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
srv, err := hksv.NewServer(hksv.Config{
StreamName: "my-camera",
Pin: "27041991",
HKSV: true,
MotionMode: "detect",
Streams: &myStreamProvider{},
Store: &myPairingStore{},
Snapshots: &mySnapshotProvider{},
Logger: logger,
Port: 8080,
})
if err != nil {
logger.Fatal().Err(err).Msg("failed to create server")
}
// Register HAP endpoints
http.HandleFunc(hap.PathPairSetup, func(w http.ResponseWriter, r *http.Request) {
srv.Handle(w, r)
})
http.HandleFunc(hap.PathPairVerify, func(w http.ResponseWriter, r *http.Request) {
srv.Handle(w, r)
})
// Advertise via mDNS
entry := srv.MDNSEntry()
go mdns.Serve(mdns.ServiceHAP, []*mdns.ServiceEntry{entry})
// Start HTTP server
logger.Info().Msg("HomeKit camera running on :8080")
http.ListenAndServe(":8080", nil)
}
HKSV Camera with Live Streaming
srv, err := hksv.NewServer(hksv.Config{
StreamName: "my-camera",
Pin: "27041991",
HKSV: true,
MotionMode: "detect",
// Required interfaces
Streams: &myStreamProvider{},
Store: &myPairingStore{},
Snapshots: &mySnapshotProvider{},
LiveStream: &myLiveStreamHandler{}, // enables live view in Home app
Logger: logger,
Port: 8080,
})
Basic Camera (no HKSV, live streaming only)
srv, err := hksv.NewServer(hksv.Config{
StreamName: "basic-cam",
Pin: "27041991",
HKSV: false, // no HKSV recording
Streams: &myStreamProvider{},
LiveStream: &myLiveStreamHandler{},
Logger: logger,
Port: 8080,
})
Proxy Mode (transparent proxy for existing HomeKit camera)
srv, err := hksv.NewServer(hksv.Config{
StreamName: "proxied-cam",
Pin: "27041991",
ProxyURL: "homekit://192.168.1.100:51827?device_id=AA:BB:CC:DD:EE:FF&...",
Logger: logger,
Port: 8080,
})
HomeKit Doorbell
srv, err := hksv.NewServer(hksv.Config{
StreamName: "my-doorbell",
Pin: "27041991",
CategoryID: "doorbell", // creates doorbell accessory
HKSV: true,
MotionMode: "detect",
Streams: &myStreamProvider{},
Store: &myPairingStore{},
Snapshots: &mySnapshotProvider{},
Logger: logger,
Port: 8080,
})
// Trigger doorbell press from external event
srv.TriggerDoorbell()
Interfaces
The library uses dependency injection via four interfaces. You implement these to connect hksv to your own stream management, storage, and media pipeline.
StreamProvider (required)
Connects HKSV consumers to your video/audio streams.
type StreamProvider interface {
// AddConsumer connects a consumer to the named stream.
// The consumer implements core.Consumer (AddTrack, WriteTo, Stop).
AddConsumer(streamName string, consumer core.Consumer) error
// RemoveConsumer disconnects a consumer from the named stream.
RemoveConsumer(streamName string, consumer core.Consumer)
}
Example implementation:
type myStreamProvider struct {
streams map[string]*Stream // your stream registry
}
func (p *myStreamProvider) AddConsumer(name string, cons core.Consumer) error {
stream, ok := p.streams[name]
if !ok {
return fmt.Errorf("stream not found: %s", name)
}
return stream.AddConsumer(cons)
}
func (p *myStreamProvider) RemoveConsumer(name string, cons core.Consumer) {
if stream, ok := p.streams[name]; ok {
stream.RemoveConsumer(cons)
}
}
PairingStore (optional)
Persists HomeKit pairing data across restarts. If nil, pairings are lost on restart and the device must be re-paired.
type PairingStore interface {
SavePairings(streamName string, pairings []string) error
}
Example implementation (JSON file):
type filePairingStore struct {
path string
}
func (s *filePairingStore) SavePairings(name string, pairings []string) error {
data := map[string][]string{name: pairings}
b, err := json.Marshal(data)
if err != nil {
return err
}
return os.WriteFile(s.path, b, 0644)
}
SnapshotProvider (optional)
Generates JPEG snapshots for HomeKit /resource requests (shown in the Home app timeline and notifications). If nil, snapshots are not available.
type SnapshotProvider interface {
GetSnapshot(streamName string, width, height int) ([]byte, error)
}
Example implementation (ffmpeg):
type ffmpegSnapshotProvider struct {
streams map[string]*Stream
}
func (p *ffmpegSnapshotProvider) GetSnapshot(name string, w, h int) ([]byte, error) {
stream := p.streams[name]
if stream == nil {
return nil, errors.New("stream not found")
}
// Capture one keyframe from the stream
frame, err := stream.CaptureKeyframe()
if err != nil {
return nil, err
}
// Convert to JPEG using ffmpeg
return ffmpegToJPEG(frame, w, h)
}
LiveStreamHandler (optional)
Handles live-streaming requests from the Home app (RTP/SRTP setup). If nil, only HKSV recording is available (no live view).
type LiveStreamHandler interface {
// SetupEndpoints handles a SetupEndpoints request (HAP characteristic 118).
// Creates the RTP/SRTP consumer, returns the response value.
SetupEndpoints(conn net.Conn, offer *camera.SetupEndpointsRequest) (any, error)
// GetEndpointsResponse returns the current endpoints response (for GET requests).
GetEndpointsResponse() any
// StartStream starts RTP streaming with the given configuration.
// The connTracker is used to register/unregister the live stream connection
// on the HKSV server (for connection tracking and MarshalJSON).
StartStream(streamName string, conf *camera.SelectedStreamConfiguration, connTracker ConnTracker) error
// StopStream stops a stream matching the given session ID.
StopStream(sessionID string, connTracker ConnTracker) error
}
type ConnTracker interface {
AddConn(v any)
DelConn(v any)
}
Example implementation (SRTP-based):
type srtpLiveStreamHandler struct {
mu sync.Mutex
consumer *homekit.Consumer
srtp *srtp.Server
streams map[string]*Stream
}
func (h *srtpLiveStreamHandler) SetupEndpoints(conn net.Conn, offer *camera.SetupEndpointsRequest) (any, error) {
consumer := homekit.NewConsumer(conn, h.srtp)
consumer.SetOffer(offer)
h.mu.Lock()
h.consumer = consumer
h.mu.Unlock()
answer := consumer.GetAnswer()
v, err := tlv8.MarshalBase64(answer)
return v, err
}
func (h *srtpLiveStreamHandler) 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 *srtpLiveStreamHandler) StartStream(streamName string, conf *camera.SelectedStreamConfiguration, ct 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")
}
ct.AddConn(consumer)
stream := h.streams[streamName]
if err := stream.AddConsumer(consumer); err != nil {
return err
}
go func() {
_, _ = consumer.WriteTo(nil) // blocks until stream ends
stream.RemoveConsumer(consumer)
ct.DelConn(consumer)
}()
return nil
}
func (h *srtpLiveStreamHandler) StopStream(sessionID string, ct hksv.ConnTracker) error {
h.mu.Lock()
consumer := h.consumer
h.mu.Unlock()
if consumer != nil && consumer.SessionID() == sessionID {
_ = consumer.Stop()
}
return nil
}
Config Reference
type Config struct {
// Required
StreamName string // stream identifier (used for lookups)
Pin string // HomeKit pairing PIN, e.g. "27041991" (default)
Port uint16 // HAP HTTP port
Logger zerolog.Logger // structured logger
Streams StreamProvider // stream registry (required for HKSV/live/motion)
// Optional - server identity
Name string // mDNS display name (auto-generated from DeviceID if empty)
DeviceID string // MAC-like ID, e.g. "AA:BB:CC:DD:EE:FF" (auto-generated if empty)
DevicePrivate string // ed25519 private key hex (auto-generated if empty)
CategoryID string // "camera" (default), "doorbell", "bridge", or numeric
Pairings []string // pre-existing pairings from storage
// Optional - mode
ProxyURL string // if set, acts as transparent proxy (no local accessory)
HKSV bool // enable HKSV recording support
// Optional - motion detection
MotionMode string // "api" (external trigger), "continuous" (always on), "detect" (P-frame analysis)
MotionThreshold float64 // ratio threshold for "detect" mode (default 2.0, lower = more sensitive)
// Optional - hardware
Speaker *bool // include Speaker service for 2-way audio (default false)
// Optional - metadata
UserAgent string // for mDNS TXTModel field
Version string // for accessory firmware version
// Optional - persistence and features
Store PairingStore // nil = pairings not persisted
Snapshots SnapshotProvider // nil = no snapshot support
LiveStream LiveStreamHandler // nil = no live streaming (HKSV recording only)
}
Motion Detection
The library includes a built-in P-frame based motion detector that works without any external motion detection system.
How It Works
- During a warmup phase (30 P-frames), the detector establishes a baseline average frame size using fast EMA (alpha=0.1).
- After warmup, each P-frame size is compared against the baseline multiplied by the threshold.
- If
frame_size > baseline * threshold, motion is detected. - Motion stays active for a hold period (30 seconds) after the last trigger frame.
- After motion ends, there is a cooldown period (5 seconds) before new motion can be detected.
- The baseline is updated continuously with slow EMA (alpha=0.02) during idle periods.
- FPS is recalibrated every 150 frames for accurate hold/cooldown timing.
Motion Modes
| Mode | Description |
|---|---|
"api" |
Motion is triggered externally via srv.SetMotionDetected(true/false) |
"detect" |
Automatic P-frame analysis (starts on first Home Hub connection) |
"continuous" |
Always reports motion every 30 seconds (for testing/always-record) |
Using the MotionDetector Standalone
The MotionDetector can be used independently as a core.Consumer:
onMotion := func(detected bool) {
if detected {
log.Println("Motion started!")
// start recording, send notification, etc.
} else {
log.Println("Motion ended")
}
}
detector := hksv.NewMotionDetector(2.0, onMotion, logger)
// Attach to a stream (detector implements core.Consumer)
err := stream.AddConsumer(detector)
// Blocks until Stop() is called
go func() {
detector.WriteTo(nil)
}()
// Later, stop the detector
detector.Stop()
Server API
Motion Control
// Check current motion status
detected := srv.MotionDetected()
// Trigger motion detected (for "api" mode or external sensors)
srv.SetMotionDetected(true)
// Clear motion
srv.SetMotionDetected(false)
// Trigger doorbell press event
srv.TriggerDoorbell()
Connection Tracking
// Register a connection (for monitoring/JSON output)
srv.AddConn(conn)
// Unregister a connection
srv.DelConn(conn)
Pairing Management
// Add a new pairing (called automatically during HAP pair-setup)
srv.AddPair(clientID, publicKey, hap.PermissionAdmin)
// Remove a pairing
srv.DelPair(clientID)
// Get client's public key (used by HAP pair-verify)
pubKey := srv.GetPair(clientID)
JSON Serialization
The server implements json.Marshaler for status reporting:
b, _ := json.Marshal(srv)
// {"name":"go2rtc-A1B2","device_id":"AA:BB:CC:DD:EE:FF","paired":1,"category_id":"17","connections":[...]}
// If not paired, includes setup_code and setup_id for QR code generation
// {"name":"go2rtc-A1B2","device_id":"AA:BB:CC:DD:EE:FF","setup_code":"195-50-224","setup_id":"A1B2"}
mDNS Advertisement
entry := srv.MDNSEntry()
// Start mDNS advertisement
go mdns.Serve(mdns.ServiceHAP, []*mdns.ServiceEntry{entry})
Helper Functions
For deterministic ID generation from stream names:
// Generate a display name from a seed
name := hksv.CalcName("", "my-camera")
// => "go2rtc-A1B2" (deterministic from seed)
name = hksv.CalcName("My Camera", "")
// => "My Camera" (uses provided name)
// Generate a MAC-like device ID
deviceID := hksv.CalcDeviceID("", "my-camera")
// => "AA:BB:CC:DD:EE:FF" (deterministic from seed)
// Generate an ed25519 private key
privateKey := hksv.CalcDevicePrivate("", "my-camera")
// => []byte{...} (deterministic 64-byte ed25519 key)
// Generate a setup ID for QR codes
setupID := hksv.CalcSetupID("my-camera")
// => "A1B2"
// Convert category string to HAP constant
catID := hksv.CalcCategoryID("doorbell")
// => "18" (hap.CategoryDoorbell)
Multiple Cameras
You can run multiple HKSV cameras on a single port. Each camera gets its own mDNS entry and is resolved by hostname:
cameras := []string{"front-door", "backyard", "garage"}
var entries []*mdns.ServiceEntry
for _, name := range cameras {
srv, _ := hksv.NewServer(hksv.Config{
StreamName: name,
Pin: "27041991",
HKSV: true,
MotionMode: "detect",
Streams: provider,
Logger: logger,
Port: 8080,
})
entry := srv.MDNSEntry()
entries = append(entries, entry)
// Map hostname -> server for HTTP routing
host := entry.Host(mdns.ServiceHAP)
handlers[host] = srv
}
// Single HTTP server handles all cameras
http.HandleFunc(hap.PathPairSetup, func(w http.ResponseWriter, r *http.Request) {
if srv := handlers[r.Host]; srv != nil {
srv.Handle(w, r)
}
})
http.HandleFunc(hap.PathPairVerify, func(w http.ResponseWriter, r *http.Request) {
if srv := handlers[r.Host]; srv != nil {
srv.Handle(w, r)
}
})
go mdns.Serve(mdns.ServiceHAP, entries)
http.ListenAndServe(":8080", nil)
HKSV Recording Flow
Understanding the recording flow helps with debugging:
1. Home Hub discovers camera via mDNS
2. Home Hub connects -> PairSetup (first time) or PairVerify (subsequent)
3. On PairVerify success:
- If motion="detect": MotionDetector starts consuming the video stream
- If motion="continuous": prepareHKSVConsumer() + startContinuousMotion()
4. Motion detected -> SetMotionDetected(true) -> HAP event notification
5. Home Hub receives motion event -> sets up HDS DataStream:
- SetCharacteristic(TypeSetupDataStreamTransport) -> TCP listener created
- Home Hub connects to TCP port -> encrypted HDS connection established
- hksvSession created
6. Home Hub opens dataSend stream:
- handleOpen() -> takes prepared consumer (or creates new one)
- consumer.Activate() -> sends fMP4 init segment over HDS
- H264 keyframes trigger GOP flush -> mediaFragment sent over HDS
7. Home Hub closes dataSend -> handleClose() -> consumer stopped
8. Motion timeout -> SetMotionDetected(false)
Example CLI Application
The example/ directory contains a standalone CLI app that exports any RTSP camera as an HKSV camera in HomeKit.
Build & Run
# Run directly
go run ./pkg/hksv/example -url rtsp://camera:554/stream
# Or build a binary
go build -o hksv-camera ./pkg/hksv/example
./hksv-camera -url rtsp://admin:pass@192.168.1.100:554/h264
Flags
| Flag | Default | Description |
|---|---|---|
-url |
(required) | RTSP stream URL |
-pin |
27041991 |
HomeKit pairing PIN |
-port |
0 (auto) |
HAP HTTP port |
-motion |
detect |
Motion mode: detect, continuous, api |
-threshold |
2.0 |
Motion sensitivity (lower = more sensitive) |
-pairings |
pairings.json |
File to persist HomeKit pairings |
How It Works
- Connects to the RTSP source, discovers available tracks (H264/AAC)
- Creates an HKSV server with HAP pairing and encrypted communication
- Advertises the camera via mDNS — it appears in the Home app
- On motion detection, Home Hub opens an HDS DataStream and records fMP4 fragments
- Pairings are saved to a JSON file so the camera survives restarts
Architecture
RTSP Camera ──► rtsp.Conn (Producer)
│
▼
streamProvider ◄── hksv.Server
(AddConsumer) │ │
│ ▼ ▼
├── MotionDetector HKSVConsumer
│ (P-frame EMA) (fMP4 → HDS)
│ │ │
│ ▼ ▼
│ HAP event → Home Hub
│ motion notify records video
│
└── mDNS advertisement
Testing
# Run all tests
go test ./pkg/hksv/...
# Run with verbose output
go test -v ./pkg/hksv/...
# Run benchmarks
go test -bench=. ./pkg/hksv/...
# Run specific test
go test -v -run TestMotionDetector_BasicTrigger ./pkg/hksv/...
Requirements
- Go 1.22+
- Dependencies:
github.com/pion/rtp,github.com/rs/zerolog(plus go2rtcpkg/packages)