From 93cac5c3e47c7aa872b610d55731a11f6b215ba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wala?= Date: Wed, 6 Mar 2024 13:13:43 +0100 Subject: [PATCH] Add local/remote ICE candidates to current/pending description. (#83) --- lib/ex_webrtc/ice_transport.ex | 6 +++ lib/ex_webrtc/peer_connection.ex | 55 ++++++++++++++++++------- lib/ex_webrtc/sdp_utils.ex | 7 ++++ mix.exs | 2 +- mix.lock | 2 +- test/ex_webrtc/dtls_transport_test.exs | 6 +++ test/ex_webrtc/peer_connection_test.exs | 21 ++++++++++ 7 files changed, 81 insertions(+), 18 deletions(-) diff --git a/lib/ex_webrtc/ice_transport.ex b/lib/ex_webrtc/ice_transport.ex index 49110093..9c53e758 100644 --- a/lib/ex_webrtc/ice_transport.ex +++ b/lib/ex_webrtc/ice_transport.ex @@ -11,6 +11,8 @@ defmodule ExWebRTC.ICETransport do @callback end_of_candidates(pid()) :: :ok @callback gather_candidates(pid()) :: :ok @callback get_local_credentials(pid()) :: {:ok, ufrag :: binary(), pwd :: binary()} + @callback get_local_candidates(pid()) :: [binary()] + @callback get_remote_candidates(pid()) :: [binary()] @callback restart(pid()) :: :ok @callback send_data(pid(), binary()) :: :ok @callback set_remote_credentials(pid(), ufrag :: binary(), pwd :: binary()) :: :ok @@ -38,6 +40,10 @@ defmodule ExWebRTC.DefaultICETransport do @impl true defdelegate get_local_credentials(pid), to: ICEAgent @impl true + defdelegate get_local_candidates(pid), to: ICEAgent + @impl true + defdelegate get_remote_candidates(pid), to: ICEAgent + @impl true defdelegate restart(pid), to: ICEAgent @impl true defdelegate send_data(pid, data), to: ICEAgent diff --git a/lib/ex_webrtc/peer_connection.ex b/lib/ex_webrtc/peer_connection.ex index 4ad9077b..2a1ca2bb 100644 --- a/lib/ex_webrtc/peer_connection.ex +++ b/lib/ex_webrtc/peer_connection.ex @@ -110,6 +110,11 @@ defmodule ExWebRTC.PeerConnection do GenServer.call(peer_connection, {:set_local_description, description}) end + @spec get_local_description(peer_connection()) :: SessionDescription.t() | nil + def get_local_description(peer_connection) do + GenServer.call(peer_connection, :get_local_description) + end + @spec get_current_local_description(peer_connection()) :: SessionDescription.t() | nil def get_current_local_description(peer_connection) do GenServer.call(peer_connection, :get_current_local_description) @@ -121,6 +126,11 @@ defmodule ExWebRTC.PeerConnection do GenServer.call(peer_connection, {:set_remote_description, description}) end + @spec get_remote_description(peer_connection()) :: SessionDescription.t() | nil + def get_remote_description(peer_connection) do + GenServer.call(peer_connection, :get_remote_description) + end + @spec get_current_remote_description(peer_connection()) :: SessionDescription.t() | nil def get_current_remote_description(peer_connection) do GenServer.call(peer_connection, :get_current_remote_description) @@ -385,15 +395,18 @@ defmodule ExWebRTC.PeerConnection do end @impl true - def handle_call(:get_current_local_description, _from, state) do - case state.current_local_desc do - nil -> - {:reply, nil, state} + def handle_call(:get_local_description, _from, state) do + desc = state.pending_local_desc || state.current_local_desc + candidates = state.ice_transport.get_local_candidates(state.ice_pid) + desc = do_get_description(desc, candidates) + {:reply, desc, state} + end - {type, sdp} -> - desc = %SessionDescription{type: type, sdp: to_string(sdp)} - {:reply, desc, state} - end + @impl true + def handle_call(:get_current_local_description, _from, state) do + candidates = state.ice_transport.get_local_candidates(state.ice_pid) + desc = do_get_description(state.current_local_desc, candidates) + {:reply, desc, state} end @impl true @@ -405,15 +418,18 @@ defmodule ExWebRTC.PeerConnection do end @impl true - def handle_call(:get_current_remote_description, _from, state) do - case state.current_remote_desc do - nil -> - {:reply, nil, state} + def handle_call(:get_remote_description, _from, state) do + desc = state.pending_remote_desc || state.current_remote_desc + candidates = state.ice_transport.get_remote_candidates(state.ice_pid) + desc = do_get_description(desc, candidates) + {:reply, desc, state} + end - {type, sdp} -> - desc = %SessionDescription{type: type, sdp: to_string(sdp)} - {:reply, desc, state} - end + @impl true + def handle_call(:get_current_remote_description, _from, state) do + candidates = state.ice_transport.get_remote_candidates(state.ice_pid) + desc = do_get_description(state.current_remote_desc, candidates) + {:reply, desc, state} end @impl true @@ -1463,6 +1479,13 @@ defmodule ExWebRTC.PeerConnection do end end + defp do_get_description(nil, _candidates), do: nil + + defp do_get_description({type, sdp}, candidates) do + sdp = SDPUtils.add_ice_candidates(sdp, candidates) + %SessionDescription{type: type, sdp: to_string(sdp)} + end + defp generate_ssrc(state) do rtp_sender_ssrcs = Enum.map(state.transceivers, & &1.sender.ssrc) ssrcs = MapSet.new(Map.keys(state.demuxer.ssrc_to_mid) ++ rtp_sender_ssrcs) diff --git a/lib/ex_webrtc/sdp_utils.ex b/lib/ex_webrtc/sdp_utils.ex index e33cbb16..96b15dc5 100644 --- a/lib/ex_webrtc/sdp_utils.ex +++ b/lib/ex_webrtc/sdp_utils.ex @@ -127,6 +127,13 @@ defmodule ExWebRTC.SDPUtils do |> Enum.map(fn {"candidate", attr} -> attr end) end + @spec add_ice_candidates(ExSDP.t(), [String.t()]) :: ExSDP.t() + def add_ice_candidates(sdp, candidates) do + candidates = Enum.map(candidates, &{"candidate", &1}) + media = Enum.map(sdp.media, &ExSDP.add_attributes(&1, candidates)) + %ExSDP{sdp | media: media} + end + @spec get_dtls_role(ExSDP.t()) :: {:ok, :active | :passive | :actpass} | {:error, :missing_dtls_role | :conflicting_dtls_roles} diff --git a/mix.exs b/mix.exs index 7deaa0ef..cbbfe03a 100644 --- a/mix.exs +++ b/mix.exs @@ -51,7 +51,7 @@ defmodule ExWebRTC.MixProject do defp deps do [ {:ex_sdp, "~> 0.15.0"}, - {:ex_ice, "~> 0.6.0"}, + {:ex_ice, github: "elixir-webrtc/ex_ice"}, {:ex_dtls, "~> 0.15.0"}, {:ex_libsrtp, "~> 0.7.1"}, {:ex_rtp, "~> 0.3.0"}, diff --git a/mix.lock b/mix.lock index d0b4f9f1..df1d6d8a 100644 --- a/mix.lock +++ b/mix.lock @@ -13,7 +13,7 @@ "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_doc": {:hex, :ex_doc, "0.31.1", "8a2355ac42b1cc7b2379da9e40243f2670143721dd50748bf6c3b1184dae2089", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3178c3a407c557d8343479e1ff117a96fd31bafe52a039079593fb0524ef61b0"}, "ex_dtls": {:hex, :ex_dtls, "0.15.1", "34b3600ff13eebf6c96be033005cc110ea5beef98394631365ec26b493df80c5", [:mix], [{:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "9cfebdfe9111c0f68c77667cb9366e4a6f17e8a240975ffd100148de57478a29"}, - "ex_ice": {:hex, :ex_ice, "0.6.1", "6e28c38a750c36e8ecea359bcff8c88ed47be036d7b217f376480eccd1d2ed08", [:mix], [{:ex_stun, "~> 0.1.0", [hex: :ex_stun, repo: "hexpm", optional: false]}], "hexpm", "cdc5d0199930c2be66c473b7a60d4786c63d23a624f7f52a2b08c3ea059669fe"}, + "ex_ice": {:git, "https://github.com/elixir-webrtc/ex_ice.git", "3d98186bac257e671e4fabaf930729c3299adea7", []}, "ex_libsrtp": {:hex, :ex_libsrtp, "0.7.2", "211bd89c08026943ce71f3e2c0231795b99cee748808ed3ae7b97cd8d2450b6b", [:mix], [{:bunch, "~> 1.6", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.3", [hex: :bundlex, repo: "hexpm", optional: false]}, {:membrane_precompiled_dependency_provider, "~> 0.1.0", [hex: :membrane_precompiled_dependency_provider, repo: "hexpm", optional: false]}, {:unifex, "~> 1.1", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "2e20645d0d739a4ecdcf8d4810a0c198120c8a2f617f2b75b2e2e704d59f492a"}, "ex_rtcp": {:hex, :ex_rtcp, "0.2.0", "7599506340835d5e879ca04c6e90b25c56340ffedd4d345c14b1c0fc858446c9", [:mix], [], "hexpm", "5a5b47a2549bd2ae0bb03ec87f166f561c031ff6c8bbe189bde1e68e8578a99f"}, "ex_rtp": {:hex, :ex_rtp, "0.3.0", "d18d0de39875958902816284f79c8cd51ec83d39bdd14edf6a5b069926268a43", [:mix], [], "hexpm", "cec8398237095b02438842cfc74487c3cbaeb7fe29e4c62ae11457a0ddd99754"}, diff --git a/test/ex_webrtc/dtls_transport_test.exs b/test/ex_webrtc/dtls_transport_test.exs index 0fe6408d..33201726 100644 --- a/test/ex_webrtc/dtls_transport_test.exs +++ b/test/ex_webrtc/dtls_transport_test.exs @@ -35,6 +35,12 @@ defmodule ExWebRTC.DTLSTransportTest do @impl true def get_local_credentials(_state), do: {:ok, "testufrag", "testpwd"} + @impl true + def get_local_candidates(_ice_pid), do: [] + + @impl true + def get_remote_candidates(_ice_pid), do: [] + @impl true def restart(ice_pid), do: ice_pid diff --git a/test/ex_webrtc/peer_connection_test.exs b/test/ex_webrtc/peer_connection_test.exs index a58debb0..48848448 100644 --- a/test/ex_webrtc/peer_connection_test.exs +++ b/test/ex_webrtc/peer_connection_test.exs @@ -224,6 +224,27 @@ defmodule ExWebRTC.PeerConnectionTest do :ok = PeerConnection.close(pc2) end + describe "get_local_description/1" do + test "includes ICE candidates" do + {:ok, pc} = PeerConnection.start() + {:ok, _sender} = PeerConnection.add_transceiver(pc, :audio) + {:ok, offer} = PeerConnection.create_offer(pc) + :ok = PeerConnection.set_local_description(pc, offer) + + assert_receive {:ex_webrtc, _from, {:ice_candidate, cand}} + desc = PeerConnection.get_local_description(pc) + + assert desc != nil + + "a=" <> desc_cand = + desc.sdp + |> String.split("\r\n") + |> Enum.find(&String.starts_with?(&1, "a=candidate:")) + + assert desc_cand == cand.candidate + end + end + describe "set_remote_description/2" do test "MID" do {:ok, pc} = PeerConnection.start_link()