Add classic HomeKit Secure Video recording#493
Conversation
Implements HomeKit Secure Video (classic HKSV) so an accessory can record event-triggered fragmented-MP4 clips to a HomeKit Home Hub over the HomeKit Data Stream (HDS). Verified end-to-end against a real iOS 26.5 Home Hub: motion triggers the hub to open a recording stream and pull fMP4 fragments over the encrypted HDS connection. Contents: - HDS transport: framing + ChaCha20-Poly1305 key derivation (pyhap/hds.py), the message/opack protocol codec (hds_protocol.py), the TCP listener and connection dispatch (hds_server.py), and the dataSend recording fragment transfer (hds_recording.py). - Recording configuration TLV codecs matching the HAP spec / HAP-NodeJS (hksv_recording.py): supported vs. selected encoding, list separators, per-attribute resolutions; encode/decode are faithful inverses in both directions (single value <-> scalar, several <-> list). - Camera wiring (camera.py): CameraRecordingManagement, the Camera Operating Mode, the DataStreamTransportManagement service, an HKSV event-trigger MotionSensor, and the recording-stream delegate hook. Stream management is set up only when the options describe video, so a recording-only camera is possible (streaming_status is None for it). - Recording eligibility characteristics iOS validates before managing recording: Active (0x0B0) on CameraRTPStreamManagement, StatusActive on the MotionSensor, PeriodicSnapshotsActive on the operating mode, and uint8 timed-write HomeKitCameraActive/RecordingAudioActive. The shared Version characteristic becomes paired-read only (per the HAP spec; it previously also advertised events, which the accessory never emitted - existing to_HAP expectations updated accordingly). - SetupDataStreamTransport is a write-response characteristic: the setter returns the encoded response (TCP listening port + accessory key salt) so the HAP layer delivers it in the write response and the controller can open the HDS connection. - Integration helpers: fmp4_recording_packets() turns any fragmented-MP4 byte source (ffmpeg stdout, an aiohttp/go2rtc response, ...) into the init packet + media fragments a recording delegate yields, and set_motion_detected() drives the event trigger - so a consumer supplies a keyframe-aligned fMP4 source and a motion signal rather than reimplementing the framing. - Framework hooks: the pair-verify shared secret is exposed per session for HDS key derivation (and popped on disconnect), and setters can opt into the sender's client address (introspection cached per setter, not per write). - accessory_driver guards the asyncio.SafeChildWatcher usage which was removed in Python 3.14. Robustness (asyncio / resource management): - The pre-handshake receive buffer is capped and connections that never complete the control handshake time out and close, so an unauthenticated peer on the advertised port cannot make the process buffer unboundedly. Frames announcing more than the 1 MiB payload cap are rejected. - Pending transport registrations are bounded; malformed frames/messages and handler errors are contained (bounded opack recursion, guarded dispatch) rather than tearing down the link uncaught. A stale dataSend close for an earlier stream cannot stop the current one. - The recording task and its delegate are torn down exactly once on dataSend close and/or connection loss (a close-callback list, not a single slot; a second cancel cannot abort the in-flight generator aclose()), and the delegate's async generator is aclose()d so its subprocess is released deterministically. - Fragment delivery honours transport write back-pressure (pause/resume) so a slow controller cannot make the accessory buffer a whole fMP4 fragment. Tests: HDS fragment transfer and chunking, connection-loss cleanup, the close-then-disconnect double-stop, write back-pressure, pre-bind buffer cap and pending bounds, recording configuration round-trips (incl. multi-value), recording management setup/eligibility, and the SetupDataStreamTransport write-response handshake (through the HAP path, plus a single-argument-setter regression guard).
|
Some context on where this fits: This implements the classic HKSV recording path — HDS + It's also intended as the foundation for Apple's newer HomeKit Secure Video spec — the HomeKit Secure Video Open Source Compatibility Guide (HEVC / multi-tier RTP, WebRTC, CMAF-ingest recording). That path keeps the classic H.264 recording configuration as the baseline and layers HEVC tiers on top, so a working classic HKSV implementation is the prerequisite. Landing this first gives HKSV a complete, working baseline now, with the HEVC / multi-tier layer as a follow-up. |
Summary
Adds HomeKit Secure Video (classic HKSV) to the
Cameraaccessory: an accessory can now record event-triggered fragmented-MP4 clips to a HomeKit Home Hub over the HomeKit Data Stream (HDS).Verified end-to-end against a real iOS 26.5 Home Hub — motion triggers the hub to open a recording stream and pull fMP4 fragments over the encrypted HDS connection.
Non-breaking and opt-in: existing cameras are unaffected; recording is enabled via a
recordingoption.What's included
hds.py), the opack message codec (hds_protocol.py), the TCP listener/dispatch (hds_server.py), thedataSendrecording fragment transfer (hds_recording.py).hksv_recording.py) matching the HAP spec / HAP-NodeJS (supported vs. selected encoding, list separators, per-attribute resolutions).CameraRecordingManagement,CameraOperatingMode,DataStreamTransportManagement, an event-triggerMotionSensor, the recording-stream delegate hook. Streaming is set up only when video options are present, so a recording-only camera is possible.wrmechanism).fmp4_recording_packets(reader)turns any fragmented-MP4 byte source (ffmpeg stdout, an aiohttp/go2rtc body, ...) into the init packet + fragments a delegate yields;Camera.set_motion_detected(bool)drives the trigger. A consumer supplies a keyframe-aligned fMP4 source and a motion signal rather than reimplementing framing/protocol.Robustness (asyncio / resources)
dataSendclose can't stop the current stream.aclose()d so its subprocess is released deterministically.pause/resume_writing).Testing
flake8+pylint(repo configs) clean;black/isortapplied.Notes
Versioncharacteristic (0x37) is narrowed to paired-read only (per spec; it previously also advertised events the accessory never emitted). Twoto_HAPtest expectations updated accordingly.asyncio.SafeChildWatcher.CHANGELOG.mdentry if you'd like it in this PR — left it out so you can place it under the version/section you prefer.