Skip to content

Commit

Permalink
Add simulcast and rid attributes (#49)
Browse files Browse the repository at this point in the history
* Add `simulcast` and `rid` attributes

* Add tests

* Bump version

* Apply requested changes
  • Loading branch information
LVala authored May 29, 2024
1 parent 6eacd4f commit 3b57c73
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 3 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
17 changes: 16 additions & 1 deletion lib/ex_sdp/attribute.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -51,6 +62,8 @@ defmodule ExSDP.Attribute do
| RTPMapping.t()
| SSRC.t()
| SSRCGroup.t()
| RID.t()
| Simulcast.t()
| cat()
| charset()
| keywds()
Expand Down Expand Up @@ -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}}
Expand Down
96 changes: 96 additions & 0 deletions lib/ex_sdp/attribute/rid.ex
Original file line number Diff line number Diff line change
@@ -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
73 changes: 73 additions & 0 deletions lib/ex_sdp/attribute/simulcast.ex
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions lib/ex_sdp/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ defmodule ExSDP.Utils do
FMTP,
Group,
MSID,
RID,
RTCPFeedback,
RTPMapping,
Simulcast,
SSRC,
SSRCGroup
}
Expand All @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -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
Expand Down
45 changes: 45 additions & 0 deletions test/ex_sdp/attribute/rid_test.exs
Original file line number Diff line number Diff line change
@@ -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
29 changes: 29 additions & 0 deletions test/ex_sdp/attribute/simulcast_test.exs
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 3b57c73

Please sign in to comment.