From 3b57c7318b98c1cbca5aaecce525e901fb6975c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wala?= Date: Wed, 29 May 2024 11:50:26 +0200 Subject: [PATCH] Add `simulcast` and `rid` attributes (#49) * Add `simulcast` and `rid` attributes * Add tests * Bump version * Apply requested changes --- README.md | 2 +- lib/ex_sdp/attribute.ex | 17 ++++- lib/ex_sdp/attribute/rid.ex | 96 ++++++++++++++++++++++++ lib/ex_sdp/attribute/simulcast.ex | 73 ++++++++++++++++++ lib/ex_sdp/utils.ex | 4 + mix.exs | 2 +- test/ex_sdp/attribute/rid_test.exs | 45 +++++++++++ test/ex_sdp/attribute/simulcast_test.exs | 29 +++++++ 8 files changed, 265 insertions(+), 3 deletions(-) create mode 100644 lib/ex_sdp/attribute/rid.ex create mode 100644 lib/ex_sdp/attribute/simulcast.ex create mode 100644 test/ex_sdp/attribute/rid_test.exs create mode 100644 test/ex_sdp/attribute/simulcast_test.exs diff --git a/README.md b/README.md index 2b909ac..31a1932 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ The package can be installed by adding `ex_sdp` to your list of dependencies in ```elixir def deps do [ - {:ex_sdp, "~> 0.16.0"} + {:ex_sdp, "~> 0.17.0"} ] end ``` diff --git a/lib/ex_sdp/attribute.ex b/lib/ex_sdp/attribute.ex index 3d548f2..e203fb6 100644 --- a/lib/ex_sdp/attribute.ex +++ b/lib/ex_sdp/attribute.ex @@ -4,7 +4,18 @@ defmodule ExSDP.Attribute do """ use Bunch.Access - alias __MODULE__.{Extmap, FMTP, Group, MSID, RTCPFeedback, RTPMapping, SSRC, SSRCGroup} + alias __MODULE__.{ + Extmap, + FMTP, + Group, + MSID, + RTCPFeedback, + RTPMapping, + SSRC, + SSRCGroup, + RID, + Simulcast + } @type hash_function :: :sha1 | :sha224 | :sha256 | :sha384 | :sha512 @type setup_value :: :active | :passive | :actpass | :holdconn @@ -51,6 +62,8 @@ defmodule ExSDP.Attribute do | RTPMapping.t() | SSRC.t() | SSRCGroup.t() + | RID.t() + | Simulcast.t() | cat() | charset() | keywds() @@ -100,6 +113,8 @@ defmodule ExSDP.Attribute do defp do_parse("group", value, _opts), do: Group.parse(value) defp do_parse("extmap", value, _opts), do: Extmap.parse(value) defp do_parse("rtcp-fb", value, _opts), do: RTCPFeedback.parse(value) + defp do_parse("rid", value, _opts), do: RID.parse(value) + defp do_parse("simulcast", value, _opts), do: Simulcast.parse(value) # Flag allowing to mix one- and two-byte header extensions defp do_parse("extmap-allow-mixed", nil, _opts), do: {:ok, :extmap_allow_mixed} defp do_parse("cat", value, _opts), do: {:ok, {:cat, value}} diff --git a/lib/ex_sdp/attribute/rid.ex b/lib/ex_sdp/attribute/rid.ex new file mode 100644 index 0000000..980a174 --- /dev/null +++ b/lib/ex_sdp/attribute/rid.ex @@ -0,0 +1,96 @@ +defmodule ExSDP.Attribute.RID do + @moduledoc """ + This module represents rid (RFC 8851). + """ + + @enforce_keys [:id, :direction] + defstruct @enforce_keys ++ [pt: nil, restrictions: []] + + @type t :: %__MODULE__{ + id: binary(), + direction: :send | :recv, + pt: [non_neg_integer()] | nil, + restrictions: {String.t(), String.t()} + } + + @typedoc """ + Key that can be used for searching this attribute using `ExSDP.Media.get_attribute/2`. + """ + @type attr_key :: :rid + + @spec parse(binary()) :: {:ok, t()} | {:error, :invalid_rid} + def parse(rid) do + case String.split(rid, " ") do + [id, dir] when dir in ["send", "recv"] -> {:ok, id, dir, ""} + [id, dir, rests] when dir in ["send", "recv"] -> {:ok, id, dir, rests} + _other -> {:error, :invalid_rid} + end + |> case do + {:ok, id, dir, rests} -> + {pt, rests} = parse_restrictions(rests) + dir = String.to_atom(dir) + {:ok, %__MODULE__{id: id, direction: dir, pt: pt, restrictions: rests}} + + {:error, _res} = err -> + err + end + end + + defp parse_restrictions(rests) do + case String.split(rests, ";") do + ["pt=" <> pts | rests] -> + pts = + pts + |> String.split(",") + |> Enum.map(&Integer.parse(&1, 10)) + |> Enum.flat_map(fn + {int, _} -> [int] + :error -> [] + end) + + {pts, do_parse_restrictions(rests)} + + rests -> + {nil, do_parse_restrictions(rests)} + end + end + + defp do_parse_restrictions(rests) do + rests + |> Enum.flat_map(fn rest -> + case String.split(rest, "=", parts: 2) do + [restriction, value] -> [{restriction, value}] + _other -> [] + end + end) + end +end + +defimpl String.Chars, for: ExSDP.Attribute.RID do + alias ExSDP.Attribute.RID + + @impl true + def to_string(rid) do + %RID{id: id, direction: direction, pt: pt, restrictions: rests} = rid + direction = Atom.to_string(direction) + + pts = + case pt do + nil -> [] + pts -> ["pt=#{Enum.join(pts, ",")}"] + end + + rests = Enum.map(rests, fn {rest, value} -> "#{rest}=#{value}" end) + + pt_rests = + (pts ++ rests) + |> Enum.join(";") + + all = + [id, direction, pt_rests] + |> Enum.reject(&(&1 == "")) + |> Enum.join(" ") + + "rid:#{all}" + end +end diff --git a/lib/ex_sdp/attribute/simulcast.ex b/lib/ex_sdp/attribute/simulcast.ex new file mode 100644 index 0000000..cd90322 --- /dev/null +++ b/lib/ex_sdp/attribute/simulcast.ex @@ -0,0 +1,73 @@ +defmodule ExSDP.Attribute.Simulcast do + @moduledoc """ + This module represents simulcast (RFC 8853). + """ + + defstruct send: [], recv: [] + + @type rid() :: String.t() + @type t :: %__MODULE__{ + send: [rid() | [rid()]], + recv: [rid() | [rid()]] + } + + @typedoc """ + Key that can be used for searching this attribute using `ExSDP.Media.get_attribute/2`. + """ + @type attr_key :: :simulcast + + @spec parse(binary()) :: {:ok, t()} | {:error, :invalid_simulcast} + def parse(simulcast) do + case String.split(simulcast, " ") do + ["send", send] -> {:ok, "", send} + ["recv", recv] -> {:ok, recv, ""} + ["recv", recv, "send", send] -> {:ok, recv, send} + ["send", send, "recv", recv] -> {:ok, recv, send} + _other -> {:error, :invalid_simulcast} + end + |> case do + {:ok, recv, send} -> + send = parse_streams(send) + recv = parse_streams(recv) + {:ok, %__MODULE__{send: send, recv: recv}} + + {:error, _res} = err -> + err + end + end + + defp parse_streams(""), do: [] + + defp parse_streams(streams) do + streams + |> String.split(";") + |> Enum.map(&String.split(&1, ",")) + |> Enum.map(fn + [rid] -> rid + rids -> rids + end) + end +end + +defimpl String.Chars, for: ExSDP.Attribute.Simulcast do + alias ExSDP.Attribute.Simulcast + + @impl true + def to_string(simulcast) do + %Simulcast{send: send, recv: recv} = simulcast + send = encode_streams(send) + send = if(send == "", do: [], else: ["send", send]) + recv = encode_streams(recv) + recv = if(recv == "", do: [], else: ["recv", recv]) + send_recv = Enum.join(send ++ recv, " ") + + "simulcast:#{send_recv}" + end + + defp encode_streams(streams) do + Enum.map_join(streams, ";", fn + rids when is_list(rids) -> Enum.join(rids, ",") + rid -> rid + end) + end +end diff --git a/lib/ex_sdp/utils.ex b/lib/ex_sdp/utils.ex index f6335b0..ea21f79 100644 --- a/lib/ex_sdp/utils.ex +++ b/lib/ex_sdp/utils.ex @@ -8,8 +8,10 @@ defmodule ExSDP.Utils do FMTP, Group, MSID, + RID, RTCPFeedback, RTPMapping, + Simulcast, SSRC, SSRCGroup } @@ -20,8 +22,10 @@ defmodule ExSDP.Utils do :fmtp => FMTP, :group => Group, :msid => MSID, + :rid => RID, :rtcp_feedback => RTCPFeedback, :rtpmap => RTPMapping, + :simulcast => Simulcast, :ssrc => SSRC, :ssrc_group => SSRCGroup } diff --git a/mix.exs b/mix.exs index 5fdbcb8..ab94e30 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule ExSDP.MixProject do use Mix.Project - @version "0.16.0" + @version "0.17.0" @github_url "https://github.com/membraneframework/ex_sdp" def project do diff --git a/test/ex_sdp/attribute/rid_test.exs b/test/ex_sdp/attribute/rid_test.exs new file mode 100644 index 0000000..fd5f308 --- /dev/null +++ b/test/ex_sdp/attribute/rid_test.exs @@ -0,0 +1,45 @@ +defmodule ExSDP.Attribute.RIDTest do + use ExUnit.Case, async: true + + alias ExSDP.Attribute.RID + + test "parse/1" do + assert {:ok, rid} = RID.parse("l recv") + assert %RID{id: "l", direction: :recv, pt: nil, restrictions: []} == rid + + assert {:ok, rid} = RID.parse("h send pt=4,5") + assert %RID{id: "h", direction: :send, pt: [4, 5], restrictions: []} == rid + + assert {:ok, rid} = RID.parse("m send max-width=1280;max-fps=30") + + assert %RID{ + id: "m", + direction: :send, + pt: nil, + restrictions: [{"max-width", "1280"}, {"max-fps", "30"}] + } == rid + + assert {:ok, rid} = RID.parse("m recv pt=111;max-fps=30") + assert %RID{id: "m", direction: :recv, pt: [111], restrictions: [{"max-fps", "30"}]} == rid + end + + test "to_string/1" do + rid = %RID{id: "l", direction: :recv, pt: nil, restrictions: []} + assert to_string(rid) == "rid:l recv" + + rid = %RID{id: "h", direction: :send, pt: [4, 5], restrictions: []} + assert to_string(rid) == "rid:h send pt=4,5" + + rid = %RID{ + id: "m", + direction: :send, + pt: nil, + restrictions: [{"max-width", "1280"}, {"max-fps", "30"}] + } + + assert to_string(rid) == "rid:m send max-width=1280;max-fps=30" + + rid = %RID{id: "m", direction: :recv, pt: [111], restrictions: [{"max-fps", "30"}]} + assert to_string(rid) == "rid:m recv pt=111;max-fps=30" + end +end diff --git a/test/ex_sdp/attribute/simulcast_test.exs b/test/ex_sdp/attribute/simulcast_test.exs new file mode 100644 index 0000000..cedcb2d --- /dev/null +++ b/test/ex_sdp/attribute/simulcast_test.exs @@ -0,0 +1,29 @@ +defmodule ExSDP.Attribute.SimulcastTest do + use ExUnit.Case, async: true + + alias ExSDP.Attribute.Simulcast + + test "parse/1" do + assert {:ok, simulcast} = Simulcast.parse("recv l;h;m send l;h") + assert %Simulcast{recv: ["l", "h", "m"], send: ["l", "h"]} == simulcast + + assert {:ok, simulcast} = Simulcast.parse("send l;h;m") + assert %Simulcast{recv: [], send: ["l", "h", "m"]} == simulcast + + assert {:ok, simulcast} = Simulcast.parse("send l;m;3 recv 1,2;5") + assert %Simulcast{recv: [["1", "2"], "5"], send: ["l", "m", "3"]} == simulcast + + assert {:error, :invalid_simulcast} = Simulcast.parse("send l;h,5, rec") + end + + test "to_string/1" do + simulcast = %Simulcast{recv: ["l", "h", "m"], send: ["l", "h"]} + assert to_string(simulcast) == "simulcast:send l;h recv l;h;m" + + simulcast = %Simulcast{recv: [], send: ["l", "h", "m"]} + assert to_string(simulcast) == "simulcast:send l;h;m" + + simulcast = %Simulcast{recv: [["1", "2"], "5"], send: ["l", "m", "3"]} + assert to_string(simulcast) == "simulcast:send l;m;3 recv 1,2;5" + end +end