AirPlay.V2.PtpBmca (AirPlay v0.5.1)

Copy Markdown View Source

AirPlay 2 gPTP BMCA-yield worker.

This follows the audible airplay2-rs/HomePod flow:

  • bind PTP event/general sockets on 319/320
  • send Mac-style Sync + Announce + Signaling with priority 250
  • yield to the receiver's better clock
  • act as a PTP slave and derive the receiver-clock offset

HomePods never answer our Delay_Req, so the classic two-way exchange cannot complete. Instead the offset is taken one-way from each Sync/Follow_Up pair (t1 - t2), which is what lets receiver_time_ns/2 track the receiver's own PTP clock — a HomePod's clock is its uptime, nowhere near the host's wall clock, so an un-converged (zero) offset stamps audio decades into the receiver's future and it plays nothing.

Summary

Functions

Block until the one-way offset has converged, or timeout_ms elapses.

Return the last measured clock offset.

Convert local wall-clock time to the receiver's PTP timeline.

Start the BMCA-yield worker for a receiver host.

Stop a BMCA worker.

Types

t()

@type t() :: %AirPlay.V2.PtpBmca{clock_id: binary() | nil, pid: pid(), state: pid()}

Functions

await_sync(session, timeout_ms)

@spec await_sync(t(), non_neg_integer()) :: :ok

Block until the one-way offset has converged, or timeout_ms elapses.

Streaming before the offset converges stamps audio in the host's wall clock rather than the receiver's clock, which the receiver buffers far in its future and never plays. Always returns :ok (callers proceed either way).

offset(ptp_bmca)

@spec offset(t()) :: map()

Return the last measured clock offset.

receiver_time_ns(session, local_ns \\ now_ns())

@spec receiver_time_ns(t(), integer()) :: non_neg_integer()

Convert local wall-clock time to the receiver's PTP timeline.

start_link(host, opts \\ [])

@spec start_link(
  String.t() | :inet.ip_address(),
  keyword()
) :: {:ok, t()} | {:error, term()}

Start the BMCA-yield worker for a receiver host.

stop(ptp_bmca)

@spec stop(t()) :: :ok

Stop a BMCA worker.