mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2026-04-22 15:47:06 +08:00
feat(hksv): add example CLI application for RTSP to HomeKit Secure Video streaming
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
// Example CLI application that exports an RTSP camera stream as a HomeKit
|
||||
// Secure Video (HKSV) camera using the pkg/hksv library.
|
||||
//
|
||||
// Author: Sergei "svk" Krashevich <svk@svk.su>
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./pkg/hksv/example -url rtsp://camera:554/stream
|
||||
// go run ./pkg/hksv/example -url rtsp://admin:pass@192.168.1.100:554/h264
|
||||
//
|
||||
// Then open the Home app on your iPhone/iPad, tap "+" → "Add Accessory",
|
||||
// and scan the QR code or enter the PIN manually (default: 270-41-991).
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hksv"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mdns"
|
||||
"github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func main() {
|
||||
streamURL := flag.String("url", "", "RTSP stream URL (required)")
|
||||
pin := flag.String("pin", "27041991", "HomeKit pairing PIN")
|
||||
port := flag.Int("port", 0, "HAP HTTP port (0 = auto)")
|
||||
motion := flag.String("motion", "detect", "Motion mode: detect, continuous, api")
|
||||
threshold := flag.Float64("threshold", 2.0, "Motion detection threshold (lower = more sensitive)")
|
||||
pairFile := flag.String("pairings", "pairings.json", "Pairings persistence file")
|
||||
flag.Parse()
|
||||
|
||||
if *streamURL == "" {
|
||||
fmt.Fprintln(os.Stderr, "Usage: hksv-camera -url rtsp://camera/stream")
|
||||
flag.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
log := zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).With().Timestamp().Logger()
|
||||
|
||||
// 1. Connect to RTSP source
|
||||
client := rtsp.NewClient(*streamURL)
|
||||
if err := client.Dial(); err != nil {
|
||||
log.Fatal().Err(err).Msg("RTSP dial failed")
|
||||
}
|
||||
if err := client.Describe(); err != nil {
|
||||
log.Fatal().Err(err).Msg("RTSP describe failed")
|
||||
}
|
||||
|
||||
log.Info().Str("url", *streamURL).Int("tracks", len(client.Medias)).Msg("RTSP connected")
|
||||
|
||||
// Pre-setup all recvonly tracks so consumers can share receivers
|
||||
for _, media := range client.Medias {
|
||||
if media.Direction == core.DirectionRecvonly && len(media.Codecs) > 0 {
|
||||
if _, err := client.GetTrack(media, media.Codecs[0]); err != nil {
|
||||
log.Warn().Err(err).Str("media", media.String()).Msg("track setup failed")
|
||||
} else {
|
||||
log.Info().Str("media", media.String()).Msg("track ready")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Listen for HAP connections
|
||||
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("listen failed")
|
||||
}
|
||||
actualPort := uint16(ln.Addr().(*net.TCPAddr).Port)
|
||||
|
||||
// 3. Load saved pairings
|
||||
store := &filePairingStore{path: *pairFile}
|
||||
pairings := store.Load()
|
||||
|
||||
// 4. Create HKSV server
|
||||
srv, err := hksv.NewServer(hksv.Config{
|
||||
StreamName: "camera",
|
||||
Pin: *pin,
|
||||
HKSV: true,
|
||||
MotionMode: *motion,
|
||||
MotionThreshold: *threshold,
|
||||
Streams: &streamProvider{client: client, log: log},
|
||||
Store: store,
|
||||
Pairings: pairings,
|
||||
Logger: log,
|
||||
Port: actualPort,
|
||||
UserAgent: "hksv-example",
|
||||
Version: "1.0.0",
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("server create failed")
|
||||
}
|
||||
|
||||
// 5. Start mDNS advertisement
|
||||
go func() {
|
||||
if err := mdns.Serve(mdns.ServiceHAP, []*mdns.ServiceEntry{srv.MDNSEntry()}); err != nil {
|
||||
log.Error().Err(err).Msg("mDNS failed")
|
||||
}
|
||||
}()
|
||||
|
||||
// 6. Start RTSP streaming (after everything is set up)
|
||||
go func() {
|
||||
if err := client.Start(); err != nil {
|
||||
log.Error().Err(err).Msg("RTSP stream ended")
|
||||
}
|
||||
}()
|
||||
|
||||
// 7. Start HTTP server for HAP protocol
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc(hap.PathPairSetup, srv.Handle)
|
||||
mux.HandleFunc(hap.PathPairVerify, srv.Handle)
|
||||
go func() {
|
||||
if err := http.Serve(ln, mux); err != nil {
|
||||
log.Fatal().Err(err).Msg("HTTP server failed")
|
||||
}
|
||||
}()
|
||||
|
||||
// Print server info
|
||||
info, _ := json.MarshalIndent(srv, "", " ")
|
||||
fmt.Fprintf(os.Stderr, "\nHomeKit camera ready on port %d\n%s\n\n", actualPort, info)
|
||||
|
||||
// Wait for shutdown signal
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sig
|
||||
|
||||
log.Info().Msg("shutting down")
|
||||
_ = client.Stop()
|
||||
}
|
||||
|
||||
// streamProvider connects HKSV consumers to the RTSP producer.
|
||||
// It implements hksv.StreamProvider.
|
||||
type streamProvider struct {
|
||||
client *rtsp.Conn
|
||||
log zerolog.Logger
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (p *streamProvider) AddConsumer(_ string, cons core.Consumer) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
var matched int
|
||||
|
||||
for _, consMedia := range cons.GetMedias() {
|
||||
if consMedia.Direction != core.DirectionSendonly {
|
||||
continue
|
||||
}
|
||||
for _, prodMedia := range p.client.Medias {
|
||||
prodCodec, consCodec := prodMedia.MatchMedia(consMedia)
|
||||
if prodCodec == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
track, err := p.client.GetTrack(prodMedia, prodCodec)
|
||||
if err != nil {
|
||||
p.log.Warn().Err(err).Str("codec", prodCodec.Name).Msg("get track failed")
|
||||
continue
|
||||
}
|
||||
|
||||
if err := cons.AddTrack(consMedia, consCodec, track); err != nil {
|
||||
p.log.Warn().Err(err).Str("codec", consCodec.Name).Msg("add track failed")
|
||||
continue
|
||||
}
|
||||
|
||||
matched++
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if matched == 0 {
|
||||
return fmt.Errorf("no matching codecs between RTSP stream and consumer")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *streamProvider) RemoveConsumer(_ string, _ core.Consumer) {}
|
||||
|
||||
// filePairingStore persists HomeKit pairings to a JSON file.
|
||||
type filePairingStore struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func (s *filePairingStore) Load() []string {
|
||||
data, err := os.ReadFile(s.path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var pairings []string
|
||||
_ = json.Unmarshal(data, &pairings)
|
||||
return pairings
|
||||
}
|
||||
|
||||
func (s *filePairingStore) SavePairings(_ string, pairings []string) error {
|
||||
data, err := json.Marshal(pairings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(s.path, data, 0644)
|
||||
}
|
||||
Reference in New Issue
Block a user