Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add simulcast and rid attributes #49

Merged
merged 4 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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