lib/ex_sdp/attribute/rtcp_feedback.ex

defmodule ExSDP.Attribute.RTCPFeedback do
  @moduledoc """
  This module describes what kind(s) of RTCP Feedback will be used (RFC 4585).

  The example lines that are parsed by this module are (`a=rtcp-fb:` prefix will be handled by `ExSDP.Attribute`)

      a=rtcp-fb:* nack
      a=rtcp-fb:96 goog-remb
      a=rtcp-fb:96 transport-cc
      a=rtcp-fb:96 ccm fir
      a=rtcp-fb:96 nack
      a=rtcp-fb:96 nack pli

  """

  alias ExSDP.Attribute.RTPMapping
  alias ExSDP.Utils

  @type t :: %__MODULE__{
          pt: RTPMapping.payload_type_t() | :all,
          feedback_type: feedback_type_t()
        }

  @enforce_keys [:pt, :feedback_type]
  defstruct @enforce_keys

  @typedoc """
  Key that can be used for searching this attribute using `ExSDP.Media.get_attribute/2`.
  """
  @type attr_key :: :rtcp_feedback

  @type feedback_type_t() :: :nack | :fir | :pli | :twcc | :remb | binary()

  @fb_type_mapping %{
    "nack" => :nack,
    "ccm fir" => :fir,
    "nack pli" => :pli,
    "transport-cc" => :twcc,
    "goog-remb" => :remb
  }

  @reverse_fb_type_mapping Map.new(@fb_type_mapping, fn {k, v} -> {v, k} end)

  @spec parse(binary()) :: {:ok, t()} | {:error, :invalid_pt} | {:error, :invalid_rtcp_feedback}
  def parse(rtcp_fb) do
    with [pt_string, rest] <- String.split(rtcp_fb, " ", parts: 2),
         {:ok, pt} <- parse_pt(pt_string) do
      {:ok, %__MODULE__{pt: pt, feedback_type: parse_feedback_type(rest)}}
    else
      {:error, _reason} = err -> err
      _invalid -> {:error, :invalid_rtcp_feedback}
    end
  end

  defp parse_feedback_type(fb_type_string) do
    Map.get(@fb_type_mapping, fb_type_string, fb_type_string)
  end

  @spec serialize_feedback_type(feedback_type_t()) :: String.t()
  def serialize_feedback_type(fb_type) when is_binary(fb_type), do: fb_type

  def serialize_feedback_type(fb_type) when is_atom(fb_type) do
    Map.fetch!(@reverse_fb_type_mapping, fb_type)
  end

  defp parse_pt("*"), do: {:ok, :all}
  defp parse_pt(pt_string), do: Utils.parse_payload_type(pt_string)
end

defimpl String.Chars, for: ExSDP.Attribute.RTCPFeedback do
  alias ExSDP.Attribute.RTCPFeedback

  @impl true
  def to_string(%RTCPFeedback{pt: pt, feedback_type: type}) do
    pt_string =
      case pt do
        :all -> "*"
        pt when is_integer(pt) -> Integer.to_string(pt)
      end

    "rtcp-fb:#{pt_string} #{RTCPFeedback.serialize_feedback_type(type)}"
  end
end