Repository navigation
Add the HKSV 17.99 multi-tier RTP and WebRTC stream services - #545
Merged
Merged
Conversation
Add the eight services and 27 characteristics of Apple's HomeKit Secure Video Open Source Compatibility Guide (Developer Preview 1.0) to the loader resources. Two of them, Contributing Sensors and Motion Enabled, also become optional characteristics of the Motion Sensor service. Timed write is advertised where the guide requires it. Writes to Motion Enabled and Streaming Enabled are marked admin only with an "AdminOnlyAccess": ["write"] catalog property. It is not part of the HAP representation and not enforced yet. Camera Recording Management keeps the characteristics classic recording relies on.
Third Party Camera Active is state the camera reports, so controllers read and subscribe to it but do not write it. Night Vision and Camera Operating Mode Indicator are written with timed writes. The Camera Global Operating Mode service of HKSV 17.99 uses all three. This matches the definitions in HAP-NodeJS.
Add pyhap.hksv3 with the camera capabilities, contributing sensors and camera zones values of the HomeKit Secure Video Open Source Compatibility Guide. Lists use the HAP TLV8 list encoding: one TLV per item with an empty type 0 TLV between items of the same type. Values over 255 bytes are split into 255-byte fragments, and the decoder joins a TLV to the one before it only after such a full fragment, like hksv_recording and HAP-NodeJS. Enum values a controller sends that are not known yet are kept as ints instead of failing the write, and a zone without method or a polygon without identifier is read like HAP-NodeJS does. The tests check the encoder output and the decoder against hand written vectors and a small independent parser of the list encoding.
A new "sensors" camera option takes the image sensors of the camera, each with its real dimensions, type, intent (main or package camera) and video encodings. With it the camera advertises them in the Camera Capabilities service, accepts motion zones through the Camera Motion Zones service and adds Contributing Sensors and Motion Enabled to the motion sensor. Cameras without the option are unchanged. set_motion_detected reports the sensors that saw the motion and reports none while the user turned motion detection off. Integrations read the zones from camera_zones and override motion_zones_changed and motion_enabled_changed to react. The hooks only hear about changes, not about a controller writing the same value again. Zones, their Active state and Motion Enabled are kept in the driver's accessory state like the classic recording state: restored when the camera is rebuilt and returned to their defaults when the last pairing is removed, calling the hooks for the values that change once all state is reset. Both kinds of state share one save per burst of writes. A zones write that does not decode fails and leaves the previous zones in place. A warning is logged for sensors with fewer than the three video encodings the guide requires and for motion reported for sensors that are not configured.
A new "global_operating_mode" camera option adds the Camera Global Operating Mode service with the listed optional characteristics, after all other services so their instance IDs stay the same. The settings a controller writes are persisted and reset for a new home; changes from writes and resets call global_operating_mode_changed with the setting and its value. Manually Disabled and Third Party Camera Active are device state, set through set_manually_disabled and set_third_party_camera_active. streaming_allowed combines HomeKit Camera Active, Streaming Enabled and Manually Disabled. While it is False, the camera answers SetupEndpoints with an error and does not start streams. Cameras without the option are unchanged. When the service exists, its HomeKit Camera Active alone turns the camera on or off. The value on the Camera Operating Mode service of classic recording is then ignored, so a value left there cannot keep the camera off. A change closes a running recording and calls homekit_camera_active_changed like before; writing the same value again calls no hook.
GET /characteristics returned the stored value of every characteristic, including write-only ones such as Identify or a camera key. The HAP spec only allows reads of characteristics with the paired read (pr) permission and answers everything else with -70405. HAP-NodeJS does the same. The getter is not called for such characteristics. The /accessories database already omitted their value; this adds tests for it.
Any characteristic could be subscribed to, whether or not it has the notify (ev) permission. A controller could then receive the values that another controller wrote to a write-only characteristic. Per the HAP spec, (un)subscribing a characteristic without ev now answers -70406 and leaves the subscriptions unchanged, the same as HAP-NodeJS. A value in the same request entry is not written. Some tests subscribed to Version, which has no ev permission. They now use CurrentDoorState.
A value written to a characteristic without the pw permission still reached its setter. Answer it with -70404 like HAP-NodeJS instead, without calling the characteristic, service or accessory setters. A subscription in the same entry and the other characteristics of the request are still handled.
A write carrying a pid that was never prepared on this connection, was already used or has outlived its TTL was still applied, and its event subscriptions were changed. Only characteristics without a value got -70410. The HAP specification says such a request must be ignored and answered with -70410. Do that for every characteristic in the request, as HAP-NodeJS does. The TTL is now measured with the monotonic clock, so a wall clock jump, e.g. from NTP right after boot, can't expire a timed write.
No behaviour change. The next commit fills in the response after awaiting async setters, which needs the result building in the driver and the HTTP response filling in the handler to work on their own.
A write with "r": true got the setter's return value back for any characteristic. The HAP spec ties write responses to the write response (wr) permission, and HAP-NodeJS ignores the request for other characteristics. Do the same. When a write response is sent, it becomes the characteristic's value instead of the written one, which HAP-NodeJS does as well. The sent value is the stored one, after the usual format checks. A response the characteristic can't hold is still sent and the written value is kept. The write response tests are now parametrized and also cover a wr characteristic without paired read: neither the written value nor the response can be read back.
The HAP specification requires a plain write to a characteristic with
the timed write ("tw") permission to be refused with -70410. HAP-NodeJS
does this.
A Home hub has been seen writing HomeKit Camera Active without a timed
write, and the HomeKit Secure Video guide gives Streaming Enabled and
Motion Enabled tw as well. Other controllers may not support timed
writes at all. So the check is opt-in: with enforce_timed_writes=True
the driver refuses such a write, otherwise it applies it and only logs
at debug level.
A setter that is a coroutine function was called but never awaited. With "r": true the coroutine was passed to the JSON encoder, which raised TypeError, so the whole write failed with a 500. Writes from a controller now wait for coroutine setters before they are answered, through the existing delayed response. Accessory and service setters still run first. A setter still running after the response timeout is cancelled and its characteristic answers -70408; the other characteristics keep their results. A setter that raises answers -70402, and a closed connection cancels the setters. The return value becomes the write response and the stored value under the same rules as for sync setters. The public set_characteristics() keeps its signature. Called directly, it schedules coroutine setters and answers at once. Only coroutines are awaited; a sync setter that returns a task or future works as before.
A write response of a characteristic that also has pr was stored as its value, so any paired controller could read it with GET /characteristics or /accessories. Keep it per connection instead: the connection that wrote reads its last response until it closes. Other connections read the characteristic's own value, which the write leaves as it was, also when the setter raises or returns nothing. Any new write of a connection drops its former response. Characteristics without pr keep the response as the value.
The writing connection reads an async setter's response back, other connections don't, the request stays private while the setter runs, and a response that completes as the connection closes is not kept.
# Conflicts: # pyhap/resources/characteristics.json
An accessory that keeps per-controller state, such as camera streams a controller set up, has no way to learn that the controller went away. The driver now calls Accessory.client_disconnected() from connection_lost, and a bridge passes it on to each bridged accessory. An accessory that raises is logged and does not stop the others or the rest of the connection cleanup.
The controller address of a Setup Endpoints write is now accepted only as the canonical IP of the announced version, without an IPv6 scope ID. Any other address, a missing address, port or SRTP field, a version or port field of the wrong length or an empty crypto suite answers with an error and sets up no session. set_endpoints() also returns that error answer. The default start_stream also builds its argument list by formatting each argument of the command template, instead of splitting the formatted command.
A controller writes Setup Endpoints and reads the answer back. The answer carries the session keys, so it now stays with the connection that wrote, like a write response. A characteristic with read_back_response keeps what its setter returns for the writing connection, until it writes again or closes. Other connections and /accessories read the characteristic's own value, and the written request is not left as that value either. The same now holds for any write to a readable write response characteristic, with or without r. set_endpoints() returns its answer instead of setting the value.
# Conflicts: # pyhap/accessory_driver.py # pyhap/camera.py # tests/test_accessory_driver.py # tests/test_camera.py
The Camera Multi-Tier RTP Stream Management service of the HomeKit Secure Video 17.99 guide advertises video and audio stream tiers, sets up a session with Setup Endpoints and starts and ends streams through RTP Streaming Control. Add the video and audio stream tiers and their Supported Video/Audio Stream Tiers values, the decoder for RTP Streaming Control writes and the encoder for its read back status, the Setup Endpoints request and answer, and tiers_from_sensor() to offer the video encodings of a sensor as tiers. Unknown commands are kept as plain ints, like the other enums of the module. Tiers are frozen. A video tier also carries the peak bit rate of its encoding, which is not part of the TLV. The payload types 99 and 110 are constants, so the RTP tiers, the WebRTC tiers and a WebRTC offer agree. check_audio_tier() and check_video_tiers() hold the tiers against the guide: 20 ms mono Opus, at least one video encoding, known qualities, Highest only with High and a peak not below the average. A gap of more than 10 % of the peak and peaks above the guide's bit rate table only warn, the guide recommends them. A frame between two rows of the table takes the larger one, and a frame above 4K warns. The module docstring now says the API is experimental.
A pid of another type, e.g. a list, raised a TypeError instead of answering -70410.
An SFrame key must have 16 to 64 bytes, and an m-line index decodes only up to the 16 bits it encodes with.
- A start keeps the setup timer from firing and restores the controller SSRCs if it fails. - A reoffer answer may only carry an SFrame key for a session with SFrame. - The ffmpeg delegate sends audio from a free port instead of the controller's. - Delegates are told how sessions stay alive: keep_alive for multi-tier, session.end() on media failure for WebRTC. - Client addresses are typed, the shared stream helpers are internal, and a session of another connection is logged at debug level.
# Conflicts: # pyhap/characteristic.py
A write asks for a response only with "r": true.
# Conflicts: # pyhap/accessory_driver.py
A subclass that still sets the Setup Endpoints answer as the value made it readable for every controller. Whatever the setter of a read back characteristic leaves in the value now goes to the writer's store and the former value comes back. A plain write to another write response characteristic stays readable for everyone, as in ikalchev#535. The read back test now checks the answer's status instead of the session ID.
…None An IPv6 address in another notation, e.g. upper case or without ::, is accepted in its canonical form instead of being refused. Only a scope ID, the wrong IP version or an invalid address is refused. set_endpoints sets the refusal as the value and returns nothing, as before.
It set a new default executor on the shared test loop and shut it down when the driver stopped, so later tests on the same xdist worker failed with "cannot schedule new futures after shutdown". The driver now makes its own loop and executor, as a sync start does.
# Conflicts: # pyhap/accessory_driver.py # tests/test_accessory_driver.py
# Conflicts: # tests/test_camera.py
# Conflicts: # pyhap/accessory_driver.py # tests/test_camera.py
# Conflicts: # pyhap/camera.py # pyhap/hksv3.py # tests/test_camera.py # tests/test_camera_operating_mode.py # tests/test_hksv3.py
# Conflicts: # pyhap/camera.py # tests/test_camera.py
…connection # Conflicts: # pyhap/camera.py # tests/test_accessory_driver.py # tests/test_camera.py
# Conflicts: # pyhap/camera.py # tests/test_camera.py
A subclass that wraps set_endpoints, calls super() and returns nothing got no answer once set_endpoints only returned it. set_endpoints sets the answer, or a refusal, as the value again and returns it. The driver keeps what a read back setter leaves as the value for the writer only and puts the shared value back. Neither the former value of a refused write nor the request a setter left alone counts as an answer. An async setter must return the answer, a value it sets itself is shared. Correct the write response docstring for a write without r, type the Setup Endpoints helpers, document read_back_response and share the Setup Endpoints request of the camera tests.
# Conflicts: # tests/test_camera.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds on #532, #543 and #544 (and so on #535 to #537). Until those are merged, the diff against dev includes them.
The HomeKit Secure Video guide adds two stream services next to the classic RTP stream management: Multi-Tier RTP Stream Management, which lets a controller pick between several video tiers, for example for H.265, and WebRTC Stream Management for remote viewing. This PR adds the protocol side of both. Cameras without the new options are unchanged.
pyhap/hksv3.py: TLV8 codecs for both services.pyhap/camera_stream.py: the session state machine both services share (setup, start, reconfigure, end, idle timeout, session limits).pyhap/camera_multi_tier.py:MultiTierController, aMultiTierDelegateprotocol, andFFmpegMultiTierDelegateas a simple reference that runs one ffmpeg per session.pyhap/camera_webrtc.py:WebRTCControllerand aWebRTCDelegateprotocol for solicit offer, answer, re-offer, session updates and end. Media is up to the delegate; a WebRTC media implementation follows in a separate PR.Cameraoptionsmulti_tierandwebrtcadd the services.Tested with unit tests (3.10, 3.11), ruff, ty, and live with current iOS/tvOS hubs: H.265 multi-tier streams play locally with the ffmpeg delegate. The WebRTC side was tested live together with the media implementation of the follow-up PR.
References:
AI use: written with the help of an AI coding assistant (Opus 5.5). I reviewed and tested the changes myself (pytest, ruff, ty, live with current iOS/tvOS hubs).