AirPlay 2 gPTP packet and clock helpers.
AirPlay 2 receivers use IEEE 1588 / 802.1AS timing instead of the classic RAOP NTP clock. This module contains the packet-level pieces needed by an AP2 sender:
- PTP header and timestamp parsing/encoding
- Announce / Signaling / Sync + Follow_Up packet builders
- HomePod clock sample extraction
- SETRATEANCHORTIME anchor construction
The transport loop that binds UDP ports 319/320 and maintains a live clock sample can be built on top of these primitives.
Summary
Functions
Build the AP2 SETRATEANCHORTIME body map from a remote clock sample.
Encode an AP2 SETRATEANCHORTIME body from a remote clock sample.
Build an Announce packet for BMCA.
Create an 8-byte gPTP clock identity from a seed integer.
Convert an 8-byte clock identity to the integer used by AP2 plist bodies.
Build a Delay_Req packet for offset calculation.
Encode the common 34-byte PTP header.
Encode a PTP timestamp as 48-bit seconds plus 32-bit nanoseconds.
Estimate the receiver's current PTP time from a previously observed sample.
IEEE 1588 event port. Sync and Delay_Req are sent here.
Convert nanoseconds within a second into AP2's 32-bit fixed-point fraction.
Convert nanoseconds within a second into AP2's 64-bit fixed-point fraction.
Convert nanoseconds into a PTP timestamp map.
IEEE 1588 general port. Announce, Signaling, Follow_Up and Delay_Resp use this port.
Calculate PTP offset and one-way delay from a Delay_Req exchange.
Parse a PTP packet into header, body and convenience fields.
Parse the common 34-byte PTP header.
Parse a 10-byte PTP timestamp.
Parse TLVs from a PTP body section.
Extract a remote clock sample from a Follow_Up packet.
Build a gPTP Signaling packet with interval-request and Apple extension TLVs.
Return the current implementation status.
Build a stop Signaling packet asking the receiver to stop sending timing messages.
Build a two-step Sync packet and matching Follow_Up packet.
Convert a PTP timestamp map into nanoseconds.
Types
@type clock_sample() :: %{ clock_identity: binary(), device_time_ns: non_neg_integer(), local_time_ns: integer(), sequence_id: non_neg_integer() }
@type header() :: %{ message_type: message_type(), transport_specific: non_neg_integer(), version: non_neg_integer(), message_length: non_neg_integer(), domain_number: non_neg_integer(), flags: non_neg_integer(), correction_field: integer(), source_clock_identity: binary(), source_port: non_neg_integer(), sequence_id: non_neg_integer(), control_field: non_neg_integer(), log_message_interval: integer() }
@type message_type() ::
:sync | :delay_request | :follow_up | :delay_response | :announce | :signaling
@type timestamp() :: %{seconds: non_neg_integer(), nanoseconds: non_neg_integer()}
Functions
@spec anchor(clock_sample(), non_neg_integer(), keyword()) :: map()
Build the AP2 SETRATEANCHORTIME body map from a remote clock sample.
:delay_ms places the render anchor in the future. The RTP timestamp should
match the first audio packet intended to render at that network time.
@spec anchor_plist(clock_sample(), non_neg_integer(), keyword()) :: binary()
Encode an AP2 SETRATEANCHORTIME body from a remote clock sample.
@spec announce(binary() | non_neg_integer(), non_neg_integer(), keyword()) :: binary()
Build an Announce packet for BMCA.
Lower :priority1 wins. A Mac-style sender that should yield to HomePod-class
receivers uses priority 250; HomePod commonly advertises 248.
@spec clock_identity(non_neg_integer() | binary() | nil) :: binary()
Create an 8-byte gPTP clock identity from a seed integer.
@spec clock_identity_to_integer(binary()) :: non_neg_integer()
Convert an 8-byte clock identity to the integer used by AP2 plist bodies.
@spec delay_request( binary() | non_neg_integer(), non_neg_integer(), non_neg_integer() ) :: binary()
Build a Delay_Req packet for offset calculation.
The sourcePortIdentity (clock id + port) is sent as all zeros to match the
reference sender: HomePods reply with a Delay_Resp to a zero-identity Delay_Req
but silently ignore one carrying our real clock identity, which leaves the PTP
offset stuck at 0. The clock_id argument is accepted for API symmetry but not
placed in the packet.
@spec encode_header(message_type(), non_neg_integer(), keyword()) :: binary()
Encode the common 34-byte PTP header.
@spec encode_timestamp(timestamp() | non_neg_integer()) :: binary()
Encode a PTP timestamp as 48-bit seconds plus 32-bit nanoseconds.
@spec estimate_device_time(clock_sample(), integer()) :: non_neg_integer()
Estimate the receiver's current PTP time from a previously observed sample.
@spec event_port() :: 319
IEEE 1588 event port. Sync and Delay_Req are sent here.
@spec fraction32(non_neg_integer()) :: non_neg_integer()
Convert nanoseconds within a second into AP2's 32-bit fixed-point fraction.
@spec fraction64(non_neg_integer()) :: non_neg_integer()
Convert nanoseconds within a second into AP2's 64-bit fixed-point fraction.
@spec from_nanoseconds(non_neg_integer()) :: timestamp()
Convert nanoseconds into a PTP timestamp map.
@spec general_port() :: 320
IEEE 1588 general port. Announce, Signaling, Follow_Up and Delay_Resp use this port.
@spec offset( non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer() ) :: map()
Calculate PTP offset and one-way delay from a Delay_Req exchange.
Parse a PTP packet into header, body and convenience fields.
Parse the common 34-byte PTP header.
Parse a 10-byte PTP timestamp.
@spec parse_tlvs(binary()) :: [ %{type: non_neg_integer(), length: non_neg_integer(), payload: binary()} ]
Parse TLVs from a PTP body section.
@spec sample_from_follow_up(binary(), integer()) :: {:ok, clock_sample()} | :error
Extract a remote clock sample from a Follow_Up packet.
@spec signaling(binary() | non_neg_integer(), non_neg_integer(), keyword()) :: binary()
Build a gPTP Signaling packet with interval-request and Apple extension TLVs.
@spec status() :: {:ok, :packet_timing_primitives}
Return the current implementation status.
@spec stop_signaling(binary() | non_neg_integer(), non_neg_integer()) :: binary()
Build a stop Signaling packet asking the receiver to stop sending timing messages.
@spec sync_follow_up( binary() | non_neg_integer(), non_neg_integer(), non_neg_integer(), keyword() ) :: {binary(), binary()}
Build a two-step Sync packet and matching Follow_Up packet.
@spec to_nanoseconds(timestamp()) :: non_neg_integer()
Convert a PTP timestamp map into nanoseconds.