Files
go2rtc/pkg/hap/camera/accessory.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

206 lines
5.0 KiB
Go

package camera
import (
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
)
func NewAccessory(manuf, model, name, serial, firmware string) *hap.Accessory {
acc := &hap.Accessory{
AID: hap.DeviceAID,
Services: []*hap.Service{
hap.ServiceAccessoryInformation(manuf, model, name, serial, firmware),
ServiceCameraRTPStreamManagement(),
//hap.ServiceHAPProtocolInformation(),
ServiceMicrophone(),
},
}
acc.InitIID()
return acc
}
func NewHKSVAccessory(manuf, model, name, serial, firmware string) *hap.Accessory {
rtpStream := ServiceCameraRTPStreamManagement()
motionSensor := ServiceMotionSensor()
operatingMode := ServiceCameraOperatingMode()
recordingMgmt := ServiceCameraEventRecordingManagement()
dataStreamMgmt := ServiceDataStreamManagement()
acc := &hap.Accessory{
AID: hap.DeviceAID,
Services: []*hap.Service{
hap.ServiceAccessoryInformation(manuf, model, name, serial, firmware),
rtpStream,
ServiceMicrophone(),
motionSensor,
operatingMode,
recordingMgmt,
dataStreamMgmt,
},
}
acc.InitIID()
// HAP-NodeJS: only RecordingManagement links to DataStreamManagement
recordingMgmt.Linked = []int{int(dataStreamMgmt.IID)}
return acc
}
func NewHKSVDoorbellAccessory(manuf, model, name, serial, firmware string) *hap.Accessory {
rtpStream := ServiceCameraRTPStreamManagement()
motionSensor := ServiceMotionSensor()
operatingMode := ServiceCameraOperatingMode()
recordingMgmt := ServiceCameraEventRecordingManagement()
dataStreamMgmt := ServiceDataStreamManagement()
doorbell := ServiceDoorbell()
acc := &hap.Accessory{
AID: hap.DeviceAID,
Services: []*hap.Service{
hap.ServiceAccessoryInformation(manuf, model, name, serial, firmware),
rtpStream,
ServiceMicrophone(),
motionSensor,
operatingMode,
recordingMgmt,
dataStreamMgmt,
doorbell,
},
}
acc.InitIID()
// HAP-NodeJS: only RecordingManagement links to DataStreamManagement
recordingMgmt.Linked = []int{int(dataStreamMgmt.IID)}
return acc
}
func ServiceMicrophone() *hap.Service {
return &hap.Service{
Type: "112", // 'Microphone'
Characters: []*hap.Character{
{
Type: "11A",
Format: hap.FormatBool,
Value: 0,
Perms: hap.EVPRPW,
//Descr: "Mute",
},
//{
// Type: "119",
// Format: hap.FormatUInt8,
// Value: 100,
// Perms: hap.EVPRPW,
// //Descr: "Volume",
// //Unit: hap.UnitPercentage,
// //MinValue: 0,
// //MaxValue: 100,
// //MinStep: 1,
//},
},
}
}
func ServiceCameraRTPStreamManagement() *hap.Service {
val120, _ := tlv8.MarshalBase64(StreamingStatus{
Status: StreamingStatusAvailable,
})
val114, _ := tlv8.MarshalBase64(SupportedVideoStreamConfiguration{
Codecs: []VideoCodecConfiguration{
{
CodecType: VideoCodecTypeH264,
CodecParams: []VideoCodecParameters{
{
ProfileID: []byte{VideoCodecProfileMain},
Level: []byte{VideoCodecLevel31, VideoCodecLevel40},
},
},
VideoAttrs: []VideoCodecAttributes{
{Width: 1920, Height: 1080, Framerate: 30},
{Width: 1280, Height: 720, Framerate: 30}, // important for iPhones
{Width: 320, Height: 240, Framerate: 15}, // apple watch
},
},
},
})
val115, _ := tlv8.MarshalBase64(SupportedAudioStreamConfiguration{
Codecs: []AudioCodecConfiguration{
{
CodecType: AudioCodecTypeOpus,
CodecParams: []AudioCodecParameters{
{
Channels: 1,
BitrateMode: AudioCodecBitrateVariable,
SampleRate: []byte{AudioCodecSampleRate16Khz},
},
},
},
},
ComfortNoiseSupport: 0,
})
val116, _ := tlv8.MarshalBase64(SupportedRTPConfiguration{
SRTPCryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80},
})
service := &hap.Service{
Type: "110", // 'CameraRTPStreamManagement'
Characters: []*hap.Character{
{
Type: TypeStreamingStatus,
Format: hap.FormatTLV8,
Value: val120,
Perms: hap.EVPR,
//Descr: "Streaming Status",
},
{
Type: TypeSupportedVideoStreamConfiguration,
Format: hap.FormatTLV8,
Value: val114,
Perms: hap.PR,
//Descr: "Supported Video Stream Configuration",
},
{
Type: TypeSupportedAudioStreamConfiguration,
Format: hap.FormatTLV8,
Value: val115,
Perms: hap.PR,
//Descr: "Supported Audio Stream Configuration",
},
{
Type: TypeSupportedRTPConfiguration,
Format: hap.FormatTLV8,
Value: val116,
Perms: hap.PR,
//Descr: "Supported RTP Configuration",
},
{
Type: "B0",
Format: hap.FormatUInt8,
Value: 1,
Perms: hap.EVPRPW,
//Descr: "Active",
//MinValue: 0,
//MaxValue: 1,
//MinStep: 1,
//ValidVal: []any{0, 1},
},
{
Type: TypeSelectedStreamConfiguration,
Format: hap.FormatTLV8,
Value: "", // important empty
Perms: hap.PRPW,
//Descr: "Selected RTP Stream Configuration",
},
{
Type: TypeSetupEndpoints,
Format: hap.FormatTLV8,
Value: "", // important empty
Perms: hap.PRPW,
//Descr: "Setup Endpoints",
},
},
}
return service
}