lib/rtp/payload_format.ex

defmodule Membrane.RTP.PayloadFormat do
  @moduledoc """
  This module contains utilities for resolving RTP default payload types,
  encoding names, clock rates and (de)payloaders.
  """

  require Membrane.RTP

  alias Membrane.RTP

  @app :membrane_rtp_format
  @format_env :__membrane_format
  @pt_env :__membrane_payload_type_mapping

  @payload_types_specs %{
    0 => %{encoding_name: :PCMU, clock_rate: 8_000},
    3 => %{encoding_name: :GSM, clock_rate: 8_000},
    4 => %{encoding_name: :G732, clock_rate: 8_000},
    5 => %{encoding_name: :DVI4, clock_rate: 8_000},
    6 => %{encoding_name: :DVI4, clock_rate: 16_000},
    7 => %{encoding_name: :LPC, clock_rate: 8_000},
    8 => %{encoding_name: :PCMA, clock_rate: 8_000},
    9 => %{encoding_name: :G722, clock_rate: 8_000},
    10 => %{encoding_name: :L16, clock_rate: 44_100},
    11 => %{encoding_name: :L16, clock_rate: 44_100},
    12 => %{encoding_name: :QCELP, clock_rate: 8_000},
    13 => %{encoding_name: :CN, clock_rate: 8_000},
    14 => %{encoding_name: :MPA, clock_rate: 90_000},
    15 => %{encoding_name: :G728, clock_rate: 8_000},
    16 => %{encoding_name: :DVI4, clock_rate: 11_025},
    17 => %{encoding_name: :DVI4, clock_rate: 22_050},
    18 => %{encoding_name: :G729, clock_rate: 8_000},
    25 => %{encoding_name: :CELB, clock_rate: 90_000},
    26 => %{encoding_name: :JPEG, clock_rate: 90_000},
    28 => %{encoding_name: :NV, clock_rate: 90_000},
    31 => %{encoding_name: :H261, clock_rate: 90_000},
    32 => %{encoding_name: :MPV, clock_rate: 90_000},
    33 => %{encoding_name: :MP2T, clock_rate: 90_000},
    34 => %{encoding_name: :H263, clock_rate: 90_000}
  }

  @enforce_keys [:encoding_name]
  defstruct @enforce_keys ++
              [
                payload_type: nil,
                payloader: nil,
                depayloader: nil,
                keyframe_detector: nil,
                frame_detector: nil
              ]

  @type t :: %__MODULE__{
          encoding_name: RTP.encoding_name_t(),
          payload_type: RTP.payload_type_t() | nil,
          payloader: module | nil,
          depayloader: module | nil,
          keyframe_detector: (binary() -> boolean()) | nil,
          frame_detector: (binary() -> boolean()) | nil
        }

  @doc false
  @spec register_static_formats() :: :ok
  def register_static_formats() do
    @payload_types_specs
    |> Enum.group_by(fn {_pt, specs} -> specs.encoding_name end, fn {pt, _specs} -> pt end)
    |> Enum.each(fn
      {name, [pt]} -> register(%__MODULE__{encoding_name: name, payload_type: pt})
      _ambiguous -> :ok
    end)
  end

  @doc """
  Returns encoding name and clock rate for given payload type, if registered.
  """
  @spec get_payload_type_mapping(RTP.payload_type_t()) :: %{
          optional(:encoding_name) => RTP.encoding_name_t(),
          optional(:clock_rate) => RTP.clock_rate_t()
        }
  def get_payload_type_mapping(payload_type) when RTP.is_payload_type_static(payload_type) do
    Map.fetch!(@payload_types_specs, payload_type)
  end

  def get_payload_type_mapping(payload_type) when RTP.is_payload_type_dynamic(payload_type) do
    get_env(@pt_env, payload_type, %{})
  end

  @doc """
  Registers default encoding name and clock rate for a dynamic payload_type
  """
  @spec register_payload_type_mapping(
          RTP.dynamic_payload_type_t(),
          RTP.encoding_name_t(),
          RTP.clock_rate_t()
        ) :: :ok | no_return()
  def register_payload_type_mapping(payload_type, encoding_name, clock_rate)
      when RTP.is_payload_type_dynamic(payload_type) do
    case fetch_env(@pt_env, payload_type) do
      {:ok, payload_format} ->
        raise "RTP payload type #{payload_type} already registered: #{inspect(payload_format)}"

      :error ->
        put_env(@pt_env, payload_type, %{encoding_name: encoding_name, clock_rate: clock_rate})
    end
  end

  @doc """
  Returns payload format registered for given encoding name.
  """
  @spec get(RTP.encoding_name_t()) :: t
  def get(encoding_name) do
    get_env(@format_env, encoding_name, %__MODULE__{encoding_name: encoding_name})
  end

  @doc """
  Registers payload format.

  Raises if some payload format field was already registered and set to different value.
  """
  @spec register(t) :: :ok | no_return
  def register(%__MODULE__{encoding_name: encoding_name} = payload_format) do
    payload_format =
      get_env(@format_env, encoding_name, %{})
      |> Map.merge(payload_format, &merge_format(encoding_name, &1, &2, &3))

    put_env(@format_env, encoding_name, payload_format)
  end

  defp merge_format(_name, _k, nil, v), do: v
  defp merge_format(_name, _k, v, nil), do: v
  defp merge_format(_name, _k, v, v), do: v

  defp merge_format(name, k, v1, v2) do
    raise "Cannot register RTP payload format #{name} field #{k} to #{inspect(v2)}, " <>
            "already registered to #{inspect(v1)}."
  end

  defp put_env(env_key, key, value) do
    env = Application.get_env(@app, env_key, %{})
    Application.put_env(@app, env_key, Map.put(env, key, value))
  end

  defp fetch_env(env_key, key) do
    Application.get_env(@app, env_key, %{}) |> Map.fetch(key)
  end

  defp get_env(env_key, key, default) do
    case fetch_env(env_key, key) do
      {:ok, value} -> value
      :error -> default
    end
  end
end