AirPlay.V2.Ptp (AirPlay v0.4.1)

Copy Markdown View Source

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.

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.

Convert a PTP timestamp map into nanoseconds.

Types

clock_sample()

@type clock_sample() :: %{
  clock_identity: binary(),
  device_time_ns: non_neg_integer(),
  local_time_ns: integer(),
  sequence_id: non_neg_integer()
}

header()

@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()
}

message_type()

@type message_type() ::
  :sync | :delay_request | :follow_up | :delay_response | :announce | :signaling

timestamp()

@type timestamp() :: %{seconds: non_neg_integer(), nanoseconds: non_neg_integer()}

Functions

anchor(sample, rtp_time, opts \\ [])

@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.

anchor_plist(sample, rtp_time, opts \\ [])

@spec anchor_plist(clock_sample(), non_neg_integer(), keyword()) :: binary()

Encode an AP2 SETRATEANCHORTIME body from a remote clock sample.

announce(clock_id, sequence_id, opts \\ [])

@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.

clock_identity(seed \\ nil)

@spec clock_identity(non_neg_integer() | binary() | nil) :: binary()

Create an 8-byte gPTP clock identity from a seed integer.

clock_identity_to_integer(arg)

@spec clock_identity_to_integer(binary()) :: non_neg_integer()

Convert an 8-byte clock identity to the integer used by AP2 plist bodies.

delay_request(clock_id, sequence_id, timestamp_ns \\ System.system_time(:nanosecond))

@spec delay_request(
  binary() | non_neg_integer(),
  non_neg_integer(),
  non_neg_integer()
) :: binary()

Build a Delay_Req packet for offset calculation.

encode_header(type, sequence_id, opts \\ [])

@spec encode_header(message_type(), non_neg_integer(), keyword()) :: binary()

Encode the common 34-byte PTP header.

encode_timestamp(ns)

@spec encode_timestamp(timestamp() | non_neg_integer()) :: binary()

Encode a PTP timestamp as 48-bit seconds plus 32-bit nanoseconds.

estimate_device_time(sample, local_time_ns \\ System.monotonic_time(:nanosecond))

@spec estimate_device_time(clock_sample(), integer()) :: non_neg_integer()

Estimate the receiver's current PTP time from a previously observed sample.

event_port()

@spec event_port() :: 319

IEEE 1588 event port. Sync and Delay_Req are sent here.

fraction32(nanoseconds)

@spec fraction32(non_neg_integer()) :: non_neg_integer()

Convert nanoseconds within a second into AP2's 32-bit fixed-point fraction.

fraction64(nanoseconds)

@spec fraction64(non_neg_integer()) :: non_neg_integer()

Convert nanoseconds within a second into AP2's 64-bit fixed-point fraction.

from_nanoseconds(ns)

@spec from_nanoseconds(non_neg_integer()) :: timestamp()

Convert nanoseconds into a PTP timestamp map.

general_port()

@spec general_port() :: 320

IEEE 1588 general port. Announce, Signaling, Follow_Up and Delay_Resp use this port.

offset(t1_ns, t2_ns, t3_ns, t4_ns)

Calculate PTP offset and one-way delay from a Delay_Req exchange.

parse(packet)

@spec parse(binary()) :: {:ok, map()} | :error

Parse a PTP packet into header, body and convenience fields.

parse_header(arg1)

@spec parse_header(binary()) :: {:ok, header()} | :error

Parse the common 34-byte PTP header.

parse_timestamp(arg1)

@spec parse_timestamp(binary()) :: {:ok, timestamp()} | :error

Parse a 10-byte PTP timestamp.

parse_tlvs(data)

@spec parse_tlvs(binary()) :: [
  %{type: non_neg_integer(), length: non_neg_integer(), payload: binary()}
]

Parse TLVs from a PTP body section.

sample_from_follow_up(packet, local_time_ns \\ System.monotonic_time(:nanosecond))

@spec sample_from_follow_up(binary(), integer()) :: {:ok, clock_sample()} | :error

Extract a remote clock sample from a Follow_Up packet.

signaling(clock_id, sequence_id, opts \\ [])

@spec signaling(binary() | non_neg_integer(), non_neg_integer(), keyword()) ::
  binary()

Build a gPTP Signaling packet with interval-request and Apple extension TLVs.

status()

@spec status() :: {:ok, :packet_timing_primitives}

Return the current implementation status.

stop_signaling(clock_id, sequence_id)

@spec stop_signaling(binary() | non_neg_integer(), non_neg_integer()) :: binary()

Build a stop Signaling packet asking the receiver to stop sending timing messages.

sync_follow_up(clock_id, sequence_id, timestamp_ns \\ System.system_time(:nanosecond), opts \\ [])

@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.

to_nanoseconds(map)

@spec to_nanoseconds(timestamp()) :: non_neg_integer()

Convert a PTP timestamp map into nanoseconds.