diff --git a/lib/ex_webrtc/peer_connection.ex b/lib/ex_webrtc/peer_connection.ex index 25d97e8a..a0be88ac 100644 --- a/lib/ex_webrtc/peer_connection.ex +++ b/lib/ex_webrtc/peer_connection.ex @@ -7,9 +7,15 @@ defmodule ExWebRTC.PeerConnection do alias __MODULE__.Configuration alias ExICE.ICEAgent - alias ExWebRTC.{IceCandidate, MediaStreamTrack, RTPTransceiver, SessionDescription} - import ExWebRTC.Utils + alias ExWebRTC.{ + IceCandidate, + MediaStreamTrack, + RTPTransceiver, + SessionDescription, + SDPUtils, + Utils + } @type peer_connection() :: GenServer.server() @@ -121,7 +127,7 @@ defmodule ExWebRTC.PeerConnection do ice_ufrag: ice_ufrag, ice_pwd: ice_pwd, ice_options: "trickle", - fingerprint: {:sha256, hex_dump(dtls_fingerprint)}, + fingerprint: {:sha256, Utils.hex_dump(dtls_fingerprint)}, # TODO offer will always contain actpass # and answer should contain active # see RFC 8829 sec. 5.3.1 @@ -276,8 +282,11 @@ defmodule ExWebRTC.PeerConnection do # TODO apply steps listed in RFC 8829 5.10 media = hd(sdp.media) - with {:ice_ufrag, ufrag} <- ExSDP.Media.get_attribute(media, :ice_ufrag), - {:ice_pwd, pwd} <- ExSDP.Media.get_attribute(media, :ice_pwd), + with :ok <- SDPUtils.ensure_mid(sdp), + :ok <- SDPUtils.ensure_bundle(sdp), + {:ice_ufrag, {:ice_ufrag, ufrag}} <- + {:ice_ufrag, ExSDP.Media.get_attribute(media, :ice_ufrag)}, + {:ice_pwd, {:ice_pwd, pwd}} <- {:ice_pwd, ExSDP.Media.get_attribute(media, :ice_pwd)}, {:ok, new_transceivers} <- update_transceivers(state.transceivers, sdp) do :ok = ICEAgent.set_remote_credentials(state.ice_agent, ufrag, pwd) :ok = ICEAgent.gather_candidates(state.ice_agent) @@ -297,7 +306,9 @@ defmodule ExWebRTC.PeerConnection do {:ok, %{state | current_remote_desc: sdp, transceivers: new_transceivers}} else - nil -> {:error, :missing_ice_ufrag_or_pwd} + {:ice_ufrag, nil} -> {:error, :missing_ice_ufrag} + {:ice_pwd, nil} -> {:error, :missing_ice_pwd} + other -> other end end diff --git a/lib/ex_webrtc/sdp_utils.ex b/lib/ex_webrtc/sdp_utils.ex new file mode 100644 index 00000000..d8516f78 --- /dev/null +++ b/lib/ex_webrtc/sdp_utils.ex @@ -0,0 +1,52 @@ +defmodule ExWebRTC.SDPUtils do + @moduledoc false + + @spec get_media_direction(ExSDP.Media.t()) :: + :sendrecv | :sendonly | :recvonly | :inactive | nil + def get_media_direction(media) do + Enum.find(media.attributes, fn attr -> + attr in [:sendrecv, :sendonly, :recvonly, :inactive] + end) + end + + @spec ensure_mid(ExSDP.t()) :: :ok | {:error, :missing_mid | :duplicated_mid} + def ensure_mid(sdp) do + sdp.media + |> Enum.reduce_while({:ok, []}, fn media, {:ok, acc} -> + case ExSDP.Media.get_attributes(media, :mid) do + [{:mid, mid}] -> {:cont, {:ok, [mid | acc]}} + [] -> {:halt, {:error, :missing_mid}} + other when is_list(other) -> {:halt, {:error, :duplicated_mid}} + end + end) + |> case do + {:ok, mids} -> if Enum.uniq(mids) == mids, do: :ok, else: {:error, :duplicated_mid} + error -> error + end + end + + @spec ensure_bundle(ExSDP.t()) :: + :ok + | {:error, + :non_exhaustive_bundle_group | :missing_budnle_group | :multiple_bundle_groups} + def ensure_bundle(sdp) do + groups = ExSDP.get_attributes(sdp, ExSDP.Attribute.Group) + + mline_mids = + Enum.map(sdp.media, fn media -> + {:mid, mid} = ExSDP.Media.get_attribute(media, :mid) + mid + end) + + case groups do + [%ExSDP.Attribute.Group{semantics: "BUNDLE", mids: group_mids}] -> + if group_mids -- mline_mids == [], do: :ok, else: {:error, :non_exhaustive_bundle_group} + + [] -> + {:error, :missing_bundle_group} + + other when is_list(other) -> + {:error, :multiple_bundle_groups} + end + end +end diff --git a/lib/ex_webrtc/utils.ex b/lib/ex_webrtc/utils.ex index 0581524f..2d6d9b43 100644 --- a/lib/ex_webrtc/utils.ex +++ b/lib/ex_webrtc/utils.ex @@ -7,10 +7,4 @@ defmodule ExWebRTC.Utils do |> :binary.bin_to_list() |> Enum.map_join(":", &Base.encode16(<<&1>>)) end - - def get_media_direction(media) do - Enum.find(media.attributes, fn attr -> - attr in [:sendrecv, :sendonly, :recvonly, :inactive] - end) - end end diff --git a/mix.exs b/mix.exs index e864894a..509b39a3 100644 --- a/mix.exs +++ b/mix.exs @@ -45,7 +45,8 @@ defmodule ExWebRTC.MixProject do defp deps do [ - {:ex_sdp, "~> 0.11"}, + # {:ex_sdp, "~> 0.11"}, + {:ex_sdp, github: "membraneframework/ex_sdp", branch: "get-attr"}, {:ex_ice, "~> 0.1"}, {:ex_dtls, "~> 0.13"}, diff --git a/mix.lock b/mix.lock index 2e194d91..fdacdc0b 100644 --- a/mix.lock +++ b/mix.lock @@ -11,7 +11,7 @@ "ex_doc": {:hex, :ex_doc, "0.30.6", "5f8b54854b240a2b55c9734c4b1d0dd7bdd41f71a095d42a70445c03cf05a281", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bd48f2ddacf4e482c727f9293d9498e0881597eae6ddc3d9562bd7923375109f"}, "ex_dtls": {:hex, :ex_dtls, "0.13.0", "4d7631eefc19a8820d4f79883f379ff2ad642976bda55493d4ec4e5d10d6c078", [:mix], [{:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "3ece30967006ec12a4088e60514cb08847814fba8b8a21aca3862e5d1fd4a6bc"}, "ex_ice": {:hex, :ex_ice, "0.1.0", "2653c884872d8769cf9fc655c74002a63ed6c21be1b3c2badfa42bdc74de2355", [:mix], [{:ex_stun, "~> 0.1.0", [hex: :ex_stun, repo: "hexpm", optional: false]}], "hexpm", "e2539a321f87f31997ba974d532d00511e5828f2f113b550b1ef6aa799dd2ffe"}, - "ex_sdp": {:hex, :ex_sdp, "0.11.0", "19e3af1d70b945381752db3139dfc22a19da1e9394036721449b7fb8c49fe039", [:mix], [{:bunch, "~> 1.3", [hex: :bunch, repo: "hexpm", optional: false]}, {:uuid, "~> 1.1", [hex: :uuid, repo: "hexpm", optional: false]}], "hexpm", "7a3fe42f4ec0c18de09b10464829c27482d81d9c50c21bdebdbcfe17d2046408"}, + "ex_sdp": {:git, "https://github.com/membraneframework/ex_sdp.git", "2d8dc9e2c964ed2d883a21d88547e9c5aaf0df1a", [branch: "get-attr"]}, "ex_stun": {:hex, :ex_stun, "0.1.0", "252474bf4c8519fbf4bc0fbfc6a1b846a634b1478c65dbbfb4b6ab4e33c2a95a", [:mix], [], "hexpm", "629fc8be45b624a92522f81d85ba001877b1f0745889a2419bdb678790d7480c"}, "excoveralls": {:hex, :excoveralls, "0.17.1", "83fa7906ef23aa7fc8ad7ee469c357a63b1b3d55dd701ff5b9ce1f72442b2874", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "95bc6fda953e84c60f14da4a198880336205464e75383ec0f570180567985ae0"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, diff --git a/test/peer_connection_test.exs b/test/peer_connection_test.exs index 2c424b0a..230d1ca5 100644 --- a/test/peer_connection_test.exs +++ b/test/peer_connection_test.exs @@ -95,4 +95,28 @@ defmodule ExWebRTC.PeerConnectionTest do assert_receive {:ex_webrtc, ^pc, {:track, %MediaStreamTrack{mid: "1", kind: :video}}} refute_receive {:ex_webrtc, ^pc, {:track, %MediaStreamTrack{}}} end + + test "set_remote_description/2" do + {:ok, pc} = PeerConnection.start_link() + + raw_sdp = ExSDP.new() + + mline = + ExSDP.Media.new("audio", 9, "UDP/TLS/RTP/SAVPF", [96]) + |> ExSDP.Media.add_attribute({:mid, "0"}) + + sdp = ExSDP.add_media(raw_sdp, mline) |> to_string() + offer = %SessionDescription{type: :offer, sdp: sdp} + assert {:error, :missing_bundle_group} = PeerConnection.set_remote_description(pc, offer) + + mline = ExSDP.Media.add_attribute(mline, {:mid, "1"}) + sdp = ExSDP.add_media(raw_sdp, mline) |> to_string() + offer = %SessionDescription{type: :offer, sdp: sdp} + assert {:error, :duplicated_mid} = PeerConnection.set_remote_description(pc, offer) + + mline = ExSDP.Media.new("audio", 9, "UDP/TLS/RTP/SAVPF", [96]) + sdp = ExSDP.add_media(raw_sdp, mline) |> to_string() + offer = %SessionDescription{type: :offer, sdp: sdp} + assert {:error, :missing_mid} = PeerConnection.set_remote_description(pc, offer) + end end