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
This commit is contained in:
@@ -40,10 +40,8 @@ func NewHKSVAccessory(manuf, model, name, serial, firmware string) *hap.Accessor
|
||||
}
|
||||
acc.InitIID()
|
||||
|
||||
// CameraOperatingMode links to RTPStreamManagement and RecordingManagement
|
||||
operatingMode.Linked = []int{int(rtpStream.IID), int(recordingMgmt.IID)}
|
||||
// CameraEventRecordingManagement links to DataStreamManagement and MotionSensor
|
||||
recordingMgmt.Linked = []int{int(dataStreamMgmt.IID), int(motionSensor.IID)}
|
||||
// HAP-NodeJS: only RecordingManagement links to DataStreamManagement
|
||||
recordingMgmt.Linked = []int{int(dataStreamMgmt.IID)}
|
||||
|
||||
return acc
|
||||
}
|
||||
@@ -71,10 +69,8 @@ func NewHKSVDoorbellAccessory(manuf, model, name, serial, firmware string) *hap.
|
||||
}
|
||||
acc.InitIID()
|
||||
|
||||
// CameraOperatingMode links to RTPStreamManagement and RecordingManagement
|
||||
operatingMode.Linked = []int{int(rtpStream.IID), int(recordingMgmt.IID)}
|
||||
// CameraEventRecordingManagement links to DataStreamManagement, MotionSensor, and Doorbell
|
||||
recordingMgmt.Linked = []int{int(dataStreamMgmt.IID), int(motionSensor.IID), int(doorbell.IID)}
|
||||
// HAP-NodeJS: only RecordingManagement links to DataStreamManagement
|
||||
recordingMgmt.Linked = []int{int(dataStreamMgmt.IID)}
|
||||
|
||||
return acc
|
||||
}
|
||||
|
||||
@@ -104,6 +104,49 @@ func ServiceCameraEventRecordingManagement() *hap.Service {
|
||||
},
|
||||
})
|
||||
|
||||
// Default selected recording configuration (Home Hub expects this to persist)
|
||||
val209, _ := tlv8.MarshalBase64(SelectedCameraRecordingConfiguration{
|
||||
GeneralConfig: SupportedCameraRecordingConfiguration{
|
||||
PrebufferLength: 4000,
|
||||
EventTriggerOptions: 0x01, // motion
|
||||
MediaContainerConfigurations: MediaContainerConfigurations{
|
||||
MediaContainerType: 0,
|
||||
MediaContainerParameters: MediaContainerParameters{
|
||||
FragmentLength: 4000,
|
||||
},
|
||||
},
|
||||
},
|
||||
VideoConfig: SupportedVideoRecordingConfiguration{
|
||||
CodecConfigs: []VideoRecordingCodecConfiguration{
|
||||
{
|
||||
CodecType: VideoCodecTypeH264,
|
||||
CodecParams: VideoRecordingCodecParameters{
|
||||
ProfileID: VideoCodecProfileHigh,
|
||||
Level: VideoCodecLevel40,
|
||||
Bitrate: 2000,
|
||||
IFrameInterval: 4000,
|
||||
},
|
||||
CodecAttrs: VideoCodecAttributes{Width: 1920, Height: 1080, Framerate: 30},
|
||||
},
|
||||
},
|
||||
},
|
||||
AudioConfig: SupportedAudioRecordingConfiguration{
|
||||
CodecConfigs: []AudioRecordingCodecConfiguration{
|
||||
{
|
||||
CodecType: AudioRecordingCodecTypeAACLC,
|
||||
CodecParams: []AudioRecordingCodecParameters{
|
||||
{
|
||||
Channels: 1,
|
||||
BitrateMode: []byte{AudioCodecBitrateVariable},
|
||||
SampleRate: []byte{AudioRecordingSampleRate24Khz},
|
||||
MaxAudioBitrate: []uint32{64},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return &hap.Service{
|
||||
Type: "204",
|
||||
Characters: []*hap.Character{
|
||||
@@ -134,7 +177,7 @@ func ServiceCameraEventRecordingManagement() *hap.Service {
|
||||
{
|
||||
Type: TypeSelectedCameraRecordingConfiguration,
|
||||
Format: hap.FormatTLV8,
|
||||
Value: "",
|
||||
Value: val209,
|
||||
Perms: hap.EVPRPW,
|
||||
},
|
||||
{
|
||||
|
||||
+48
-16
@@ -163,27 +163,59 @@ func (s *Session) WriteRequest(protocol, topic string, body map[string]any) (int
|
||||
return id, s.WriteMessage(header, body)
|
||||
}
|
||||
|
||||
// maxChunkSize is the maximum data chunk size for HDS media transfer (256 KiB)
|
||||
const maxChunkSize = 0x40000
|
||||
|
||||
// SendMediaInit sends the fMP4 initialization segment (ftyp+moov)
|
||||
func (s *Session) SendMediaInit(streamID int, initData []byte) error {
|
||||
return s.WriteEvent(ProtoDataSend, TopicData, map[string]any{
|
||||
"streamId": streamID,
|
||||
"packets": 1,
|
||||
"type": "mediaInitialization",
|
||||
"data": initData,
|
||||
})
|
||||
return s.sendMediaData(streamID, "mediaInitialization", initData, 1)
|
||||
}
|
||||
|
||||
// SendMediaFragment sends an fMP4 fragment (moof+mdat)
|
||||
// SendMediaFragment sends an fMP4 fragment (moof+mdat), splitting into chunks if needed
|
||||
func (s *Session) SendMediaFragment(streamID int, fragment []byte, sequence int) error {
|
||||
return s.WriteEvent(ProtoDataSend, TopicData, map[string]any{
|
||||
"streamId": streamID,
|
||||
"packets": 1,
|
||||
"type": "mediaFragment",
|
||||
"data": fragment,
|
||||
"dataSequenceNumber": sequence,
|
||||
"isLastDataChunk": true,
|
||||
"dataChunkSequenceNumber": 0,
|
||||
})
|
||||
return s.sendMediaData(streamID, "mediaFragment", fragment, sequence)
|
||||
}
|
||||
|
||||
// sendMediaData sends media data with proper HAP-NodeJS compatible packet structure.
|
||||
// Large data is split into chunks of maxChunkSize bytes.
|
||||
func (s *Session) sendMediaData(streamID int, dataType string, data []byte, sequence int) error {
|
||||
totalSize := len(data)
|
||||
chunkSeq := 1
|
||||
|
||||
for offset := 0; offset < totalSize; offset += maxChunkSize {
|
||||
end := offset + maxChunkSize
|
||||
if end > totalSize {
|
||||
end = totalSize
|
||||
}
|
||||
chunk := data[offset:end]
|
||||
isLast := end >= totalSize
|
||||
|
||||
metadata := map[string]any{
|
||||
"dataType": dataType,
|
||||
"dataSequenceNumber": sequence,
|
||||
"dataChunkSequenceNumber": chunkSeq,
|
||||
"isLastDataChunk": isLast,
|
||||
}
|
||||
if chunkSeq == 1 {
|
||||
metadata["dataTotalSize"] = totalSize
|
||||
}
|
||||
|
||||
body := map[string]any{
|
||||
"streamId": streamID,
|
||||
"packets": []any{
|
||||
map[string]any{
|
||||
"data": chunk,
|
||||
"metadata": metadata,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := s.WriteEvent(ProtoDataSend, TopicData, body); err != nil {
|
||||
return err
|
||||
}
|
||||
chunkSeq++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run processes incoming HDS messages in a loop
|
||||
|
||||
Reference in New Issue
Block a user