defmodule AirPlay.Rtp do
@moduledoc """
RAOP packet builders: audio (PT 0x60), control `sync` (0xd4), and timing
response (0xd3). All multi-byte fields are big-endian.
"""
import Bitwise, only: [&&&: 2, <<<: 2]
@doc """
An audio RTP packet: `80 [60|e0] seq ts ssrc <payload>`. `marker?` sets the
marker bit (0xe0) on the first packet of a stream.
"""
@spec audio(non_neg_integer(), non_neg_integer(), non_neg_integer(), binary(), boolean()) ::
binary()
def audio(seq, timestamp, ssrc, payload, marker? \\ false) do
pt = if marker?, do: 0xE0, else: 0x60
<<0x80, pt, seq::16, timestamp::32, ssrc::32, payload::binary>>
end
@doc """
A control `sync` packet (PT 0x54). `rtp_now` is the current RTP timestamp,
`latency` the buffer in samples (RAOP default 88200 = 2 s), `ntp` the NTP time.
`first?` sets the 0x90 lead byte on the first sync of a stream (else 0x80).
"""
@spec sync(non_neg_integer(), non_neg_integer(), non_neg_integer(), boolean()) :: binary()
def sync(rtp_now, latency, ntp, first? \\ false) do
lead = if first?, do: 0x90, else: 0x80
rtp_minus = rtp_now - latency
<<lead, 0xD4, 0x00, 0x07, rtp_minus::32, ntp::64, rtp_now::32>>
end
@doc """
An AirPlay 2 PTP-mode sync packet (PT 87).
Unlike classic RAOP sync packets, this carries receiver PTP time rather than
NTP time and includes the master clock identity learned through gPTP.
"""
@spec ptp_sync(
non_neg_integer(),
non_neg_integer(),
non_neg_integer(),
non_neg_integer(),
binary() | non_neg_integer(),
boolean()
) :: binary()
def ptp_sync(seq, current_rtp, ptp_clock_ns, next_rtp, clock_identity, first? \\ false) do
lead = if first?, do: 0x90, else: 0x80
seconds = div(ptp_clock_ns, 1_000_000_000)
nanoseconds = rem(ptp_clock_ns, 1_000_000_000)
fraction = div(nanoseconds <<< 32, 1_000_000_000)
clock_identity = clock_identity_binary(clock_identity)
<<lead, 0xD7, seq &&& 0xFFFF::16, current_rtp &&& 0xFFFFFFFF::32, seconds &&& 0xFFFFFFFF::32,
fraction::32, next_rtp &&& 0xFFFFFFFF::32, clock_identity::binary-size(8)>>
end
@doc """
A timing response (PT 0x53) to a received timing request. `origin` is the
request's transmit timestamp; `recv`/`xmit` are our NTP receive/send times.
"""
@spec timing_response(non_neg_integer(), non_neg_integer(), non_neg_integer()) :: binary()
def timing_response(origin, recv, xmit) do
<<0x80, 0xD3, 0x00, 0x07, 0::32, origin::64, recv::64, xmit::64>>
end
@doc "Parse the transmit (origin) NTP timestamp out of a timing request (PT 0x52)."
@spec timing_request_origin(binary()) :: {:ok, non_neg_integer()} | :error
def timing_request_origin(<<0x80, 0xD2, _seq::16, 0::32, _origin::64, _recv::64, xmit::64>>),
do: {:ok, xmit}
def timing_request_origin(_), do: :error
defp clock_identity_binary(<<_::64>> = identity), do: identity
defp clock_identity_binary(identity) when is_integer(identity),
do: <<identity &&& 0xFFFFFFFFFFFFFFFF::64>>
end