Files
go2rtc/internal/homekit/hksv.go
T
Sergey Krashevich 1856b7ace4 fix(homekit): fix HKSV recording by correcting HDS protocol and adding GOP buffering
The HKSV recording was failing because:
1. The dataSend.data message structure was wrong - `packets` was a flat integer
   instead of an array of objects with `data` and `metadata` fields matching
   the HAP-NodeJS specification
2. Each video/audio frame was sent as a separate mediaFragment, but Home Hub
   expects GOP-based fragments (~2-4 seconds of accumulated data)
3. Large fragments were not chunked (max 256 KiB per chunk)

Changes:
- Fix HDS dataSend.data message structure to use proper packets array with
  nested data/metadata (dataType, dataSequenceNumber, dataChunkSequenceNumber,
  isLastDataChunk, dataTotalSize)
- Add 256 KiB chunking for large media fragments
- Buffer moof+mdat pairs in hksvConsumer and flush on keyframe boundaries
  (GOP-based fragmentation)
- Pre-start consumer at pair-verify for instant init segment delivery
- Add write-response support to HAP PUT handler for ch131 DataStream setup
- Fix HAP service linking to match HAP-NodeJS reference
- Add default SelectedCameraRecordingConfiguration (ch209) value
- Start continuous motion generator at pair-verify with dedup protection
2026-03-05 06:26:06 +03:00

432 lines
10 KiB
Go

package homekit
import (
"io"
"net"
"sync"
"time"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/hap/hds"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/pion/rtp"
)
// hksvSession manages the HDS DataStream connection for HKSV recording
type hksvSession struct {
server *server
hapConn *hap.Conn
hdsConn *hds.Conn
session *hds.Session
mu sync.Mutex
consumer *hksvConsumer
}
func newHKSVSession(srv *server, hapConn *hap.Conn, hdsConn *hds.Conn) *hksvSession {
session := hds.NewSession(hdsConn)
hs := &hksvSession{
server: srv,
hapConn: hapConn,
hdsConn: hdsConn,
session: session,
}
session.OnDataSendOpen = hs.handleOpen
session.OnDataSendClose = hs.handleClose
return hs
}
func (hs *hksvSession) Run() error {
return hs.session.Run()
}
func (hs *hksvSession) Close() {
hs.mu.Lock()
defer hs.mu.Unlock()
if hs.consumer != nil {
hs.stopRecording()
}
_ = hs.session.Close()
}
func (hs *hksvSession) handleOpen(streamID int) error {
hs.mu.Lock()
defer hs.mu.Unlock()
log.Debug().Str("stream", hs.server.stream).Int("streamID", streamID).Msg("[homekit] HKSV dataSend open")
if hs.consumer != nil {
hs.stopRecording()
}
// Try to use the pre-started consumer from pair-verify
consumer := hs.server.takePreparedConsumer()
if consumer != nil {
log.Debug().Str("stream", hs.server.stream).Msg("[homekit] HKSV using prepared consumer")
hs.consumer = consumer
hs.server.AddConn(consumer)
// Activate: set the HDS session and send init + start streaming
if err := consumer.activate(hs.session, streamID); err != nil {
log.Error().Err(err).Str("stream", hs.server.stream).Msg("[homekit] HKSV activate failed")
hs.stopRecording()
return nil
}
return nil
}
// Fallback: create new consumer (will be slow ~3s)
log.Debug().Str("stream", hs.server.stream).Msg("[homekit] HKSV no prepared consumer, creating new")
consumer = newHKSVConsumer()
stream := streams.Get(hs.server.stream)
if err := stream.AddConsumer(consumer); err != nil {
log.Error().Err(err).Str("stream", hs.server.stream).Msg("[homekit] HKSV add consumer failed")
return nil
}
hs.consumer = consumer
hs.server.AddConn(consumer)
go func() {
if err := consumer.activate(hs.session, streamID); err != nil {
log.Error().Err(err).Str("stream", hs.server.stream).Msg("[homekit] HKSV activate failed")
}
}()
return nil
}
func (hs *hksvSession) handleClose(streamID int) error {
hs.mu.Lock()
defer hs.mu.Unlock()
log.Debug().Str("stream", hs.server.stream).Int("streamID", streamID).Msg("[homekit] HKSV dataSend close")
if hs.consumer != nil {
hs.stopRecording()
}
return nil
}
func (hs *hksvSession) stopRecording() {
consumer := hs.consumer
hs.consumer = nil
stream := streams.Get(hs.server.stream)
stream.RemoveConsumer(consumer)
_ = consumer.Stop()
hs.server.DelConn(consumer)
}
// hksvConsumer implements core.Consumer, generates fMP4 and sends over HDS.
// It can be pre-started without an HDS session, buffering init data until activated.
type hksvConsumer struct {
core.Connection
muxer *mp4.Muxer
mu sync.Mutex
done chan struct{}
// Set by activate() when HDS session is available
session *hds.Session
streamID int
seqNum int
active bool
start bool // waiting for first keyframe
// GOP buffer - accumulate moof+mdat pairs, flush on next keyframe
fragBuf []byte
// Pre-built init segment (built when tracks connect)
initData []byte
initErr error
initDone chan struct{} // closed when init is ready
}
func newHKSVConsumer() *hksvConsumer {
medias := []*core.Media{
{
Kind: core.KindVideo,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecH264},
},
},
{
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecAAC},
},
},
}
return &hksvConsumer{
Connection: core.Connection{
ID: core.NewID(),
FormatName: "hksv",
Protocol: "hds",
Medias: medias,
},
muxer: &mp4.Muxer{},
done: make(chan struct{}),
initDone: make(chan struct{}),
}
}
func (c *hksvConsumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
trackID := byte(len(c.Senders))
log.Debug().Str("codec", track.Codec.Name).Uint8("trackID", trackID).Msg("[homekit] HKSV AddTrack")
codec := track.Codec.Clone()
handler := core.NewSender(media, codec)
switch track.Codec.Name {
case core.CodecH264:
handler.Handler = func(packet *rtp.Packet) {
c.mu.Lock()
if !c.active {
c.mu.Unlock()
return
}
if !c.start {
if !h264.IsKeyframe(packet.Payload) {
c.mu.Unlock()
return
}
c.start = true
log.Debug().Int("payloadLen", len(packet.Payload)).Msg("[homekit] HKSV first keyframe")
} else if h264.IsKeyframe(packet.Payload) && len(c.fragBuf) > 0 {
// New keyframe = flush previous GOP as one mediaFragment
c.flushFragment()
}
b := c.muxer.GetPayload(trackID, packet)
c.fragBuf = append(c.fragBuf, b...)
c.mu.Unlock()
}
if track.Codec.IsRTP() {
handler.Handler = h264.RTPDepay(track.Codec, handler.Handler)
} else {
handler.Handler = h264.RepairAVCC(track.Codec, handler.Handler)
}
case core.CodecAAC:
handler.Handler = func(packet *rtp.Packet) {
c.mu.Lock()
if !c.active || !c.start {
c.mu.Unlock()
return
}
b := c.muxer.GetPayload(trackID, packet)
c.fragBuf = append(c.fragBuf, b...)
c.mu.Unlock()
}
if track.Codec.IsRTP() {
handler.Handler = aac.RTPDepay(handler.Handler)
}
default:
return nil // skip unsupported codecs
}
c.muxer.AddTrack(codec)
handler.HandleRTP(track)
c.Senders = append(c.Senders, handler)
// Build init segment when all expected tracks are ready (video + audio)
select {
case <-c.initDone:
// already built
default:
if len(c.Senders) >= len(c.Medias) {
initData, err := c.muxer.GetInit()
c.initData = initData
c.initErr = err
close(c.initDone)
if err != nil {
log.Error().Err(err).Msg("[homekit] HKSV GetInit failed")
} else {
log.Debug().Int("initSize", len(initData)).Int("tracks", len(c.Senders)).Msg("[homekit] HKSV init segment ready")
}
}
}
return nil
}
// activate is called when the HDS session is ready (dataSend.open).
// It sends the pre-built init segment and starts streaming.
func (c *hksvConsumer) activate(session *hds.Session, streamID int) error {
// Wait for init to be ready (should already be done if consumer was pre-started)
select {
case <-c.initDone:
case <-time.After(5 * time.Second):
return io.ErrClosedPipe
}
if c.initErr != nil {
return c.initErr
}
log.Debug().Int("initSize", len(c.initData)).Msg("[homekit] HKSV sending init segment")
if err := session.SendMediaInit(streamID, c.initData); err != nil {
return err
}
log.Debug().Msg("[homekit] HKSV init segment sent OK")
// Enable live streaming (seqNum=2 because init used seqNum=1)
c.mu.Lock()
c.session = session
c.streamID = streamID
c.seqNum = 2
c.active = true
c.mu.Unlock()
return nil
}
// flushFragment sends the accumulated GOP buffer as a single mediaFragment.
// Must be called while holding c.mu.
func (c *hksvConsumer) flushFragment() {
fragment := c.fragBuf
c.fragBuf = make([]byte, 0, len(fragment))
log.Debug().Int("fragSize", len(fragment)).Int("seq", c.seqNum).Msg("[homekit] HKSV flush fragment")
if err := c.session.SendMediaFragment(c.streamID, fragment, c.seqNum); err == nil {
c.Send += len(fragment)
}
c.seqNum++
}
func (c *hksvConsumer) WriteTo(io.Writer) (int64, error) {
<-c.done
return 0, nil
}
func (c *hksvConsumer) Stop() error {
select {
case <-c.done:
default:
close(c.done)
}
c.mu.Lock()
c.active = false
c.mu.Unlock()
return c.Connection.Stop()
}
// acceptHDS opens a TCP listener for the HDS DataStream connection from the Home Hub
func (s *server) acceptHDS(hapConn *hap.Conn, ln net.Listener, salt string) {
defer ln.Close()
if tcpLn, ok := ln.(*net.TCPListener); ok {
_ = tcpLn.SetDeadline(time.Now().Add(30 * time.Second))
}
rawConn, err := ln.Accept()
if err != nil {
log.Error().Err(err).Str("stream", s.stream).Msg("[homekit] HKSV accept failed")
return
}
defer rawConn.Close()
// Create HDS encrypted connection (controller=false, we are accessory)
hdsConn, err := hds.NewConn(rawConn, hapConn.SharedKey, salt, false)
if err != nil {
log.Error().Err(err).Str("stream", s.stream).Msg("[homekit] HKSV hds conn failed")
return
}
s.AddConn(hdsConn)
defer s.DelConn(hdsConn)
session := newHKSVSession(s, hapConn, hdsConn)
s.mu.Lock()
s.hksvSession = session
s.mu.Unlock()
defer func() {
s.mu.Lock()
if s.hksvSession == session {
s.hksvSession = nil
}
s.mu.Unlock()
session.Close()
}()
log.Debug().Str("stream", s.stream).Msg("[homekit] HKSV session started")
if err := session.Run(); err != nil {
log.Debug().Err(err).Str("stream", s.stream).Msg("[homekit] HKSV session ended")
}
}
// prepareHKSVConsumer pre-starts a consumer and adds it to the stream.
// When dataSend.open arrives, the consumer is ready immediately.
func (s *server) prepareHKSVConsumer() {
stream := streams.Get(s.stream)
if stream == nil {
return
}
consumer := newHKSVConsumer()
if err := stream.AddConsumer(consumer); err != nil {
log.Debug().Err(err).Str("stream", s.stream).Msg("[homekit] HKSV prepare consumer failed")
return
}
log.Debug().Str("stream", s.stream).Msg("[homekit] HKSV consumer prepared")
s.mu.Lock()
// Clean up any previous prepared consumer
if s.preparedConsumer != nil {
old := s.preparedConsumer
s.preparedConsumer = nil
s.mu.Unlock()
stream.RemoveConsumer(old)
_ = old.Stop()
s.mu.Lock()
}
s.preparedConsumer = consumer
s.mu.Unlock()
// Keep alive until used or timeout (60 seconds)
select {
case <-consumer.done:
// consumer was stopped (used or server closed)
case <-time.After(60 * time.Second):
// timeout: clean up unused prepared consumer
s.mu.Lock()
if s.preparedConsumer == consumer {
s.preparedConsumer = nil
s.mu.Unlock()
stream.RemoveConsumer(consumer)
_ = consumer.Stop()
log.Debug().Str("stream", s.stream).Msg("[homekit] HKSV prepared consumer expired")
} else {
s.mu.Unlock()
}
}
}
func (s *server) takePreparedConsumer() *hksvConsumer {
s.mu.Lock()
defer s.mu.Unlock()
consumer := s.preparedConsumer
s.preparedConsumer = nil
return consumer
}