Files
go2rtc/pkg/hksv/README.md
T

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 on internal/

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

  1. During a warmup phase (30 P-frames), the detector establishes a baseline average frame size using fast EMA (alpha=0.1).
  2. After warmup, each P-frame size is compared against the baseline multiplied by the threshold.
  3. If frame_size > baseline * threshold, motion is detected.
  4. Motion stays active for a hold period (30 seconds) after the last trigger frame.
  5. After motion ends, there is a cooldown period (5 seconds) before new motion can be detected.
  6. The baseline is updated continuously with slow EMA (alpha=0.02) during idle periods.
  7. 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

  1. Connects to the RTSP source, discovers available tracks (H264/AAC)
  2. Creates an HKSV server with HAP pairing and encrypted communication
  3. Advertises the camera via mDNS — it appears in the Home app
  4. On motion detection, Home Hub opens an HDS DataStream and records fMP4 fragments
  5. 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 go2rtc pkg/ packages)