Skip to content

Commit bdbd9bd

Browse files
authored
Dynamically determine ICE agent's role (#205)
1 parent 880099e commit bdbd9bd

File tree

7 files changed

+103
-11
lines changed

7 files changed

+103
-11
lines changed

lib/ex_webrtc/ice_transport.ex

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@ defmodule ExWebRTC.ICETransport do
55
@type t() :: module()
66
@type state() :: :checking | :connected | :completed | :failed
77

8-
@callback start_link(ExICE.ICEAgent.role(), Keyword.t()) :: {:ok, pid()}
8+
@callback start_link(Keyword.t()) :: {:ok, pid()}
99
@callback on_data(pid(), pid()) :: :ok
1010
@callback add_remote_candidate(pid(), candidate :: String.t()) :: :ok
1111
@callback end_of_candidates(pid()) :: :ok
1212
@callback gather_candidates(pid()) :: :ok
1313
@callback get_local_credentials(pid()) :: {:ok, ufrag :: binary(), pwd :: binary()}
1414
@callback get_local_candidates(pid()) :: [binary()]
1515
@callback get_remote_candidates(pid()) :: [binary()]
16+
@callback get_role(pid()) :: ExICE.ICEAgent.role() | nil
1617
@callback restart(pid()) :: :ok
1718
@callback send_data(pid(), binary()) :: :ok
19+
@callback set_role(pid(), ExICE.ICEAgent.role()) :: :ok
1820
@callback set_remote_credentials(pid(), ufrag :: binary(), pwd :: binary()) :: :ok
1921
@callback get_stats(pid()) :: map()
2022
@callback stop(pid()) :: :ok
@@ -28,7 +30,7 @@ defmodule ExWebRTC.DefaultICETransport do
2830
alias ExICE.ICEAgent
2931

3032
@impl true
31-
defdelegate start_link(role, opts), to: ICEAgent
33+
defdelegate start_link(opts), to: ICEAgent
3234
@impl true
3335
defdelegate on_data(pid, dst_pid), to: ICEAgent
3436
@impl true
@@ -38,6 +40,8 @@ defmodule ExWebRTC.DefaultICETransport do
3840
@impl true
3941
defdelegate gather_candidates(pid), to: ICEAgent
4042
@impl true
43+
defdelegate get_role(pid), to: ICEAgent
44+
@impl true
4145
defdelegate get_local_credentials(pid), to: ICEAgent
4246
@impl true
4347
defdelegate get_local_candidates(pid), to: ICEAgent
@@ -48,6 +52,8 @@ defmodule ExWebRTC.DefaultICETransport do
4852
@impl true
4953
defdelegate send_data(pid, data), to: ICEAgent
5054
@impl true
55+
defdelegate set_role(pid, role), to: ICEAgent
56+
@impl true
5157
defdelegate set_remote_credentials(pid, ufrag, pwd), to: ICEAgent
5258
@impl true
5359
defdelegate get_stats(pid), to: ICEAgent

lib/ex_webrtc/peer_connection.ex

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ defmodule ExWebRTC.PeerConnection do
5353
@type ice_connection_state() ::
5454
:new | :checking | :connected | :completed | :failed | :disconnected | :closed
5555

56+
@typedoc """
57+
Possible DTLS transport states.
58+
59+
For the exact meaning, refer to the [RTCDtlsTransport: state property](https://developer.mozilla.org/en-US/docs/Web/API/RTCDtlsTransport/state)
60+
"""
61+
@type dtls_transport_state() :: :new | :connecting | :connected | :failed
62+
5663
@typedoc """
5764
Possible signaling states.
5865
@@ -65,6 +72,11 @@ defmodule ExWebRTC.PeerConnection do
6572
6673
Most of the messages match the [RTCPeerConnection events](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection#events),
6774
except for:
75+
* `:dtls_transport_state_change` - traditional WebRTC implementation does not emit such event.
76+
Instead, developer can read DTLS transport state by iterating over RTP receivers/senders, and checking their
77+
DTLS transports states. See https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpSender/transport.
78+
However, because Elixir WebRTC creates a single DTLS transport for all receivers and senders, there is one generic
79+
notification for convenience and parity with other events informing about ice/signaling/connection state changes.
6880
* `:track_muted`, `:track_ended` - these match the [MediaStreamTrack events](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack#events).
6981
* `:data` - data received from DataChannel identified by its `ref`.
7082
* `:rtp` and `:rtcp` - these contain packets received by the PeerConnection. The third element of `:rtp` tuple is a simulcast RID and is set to `nil` if simulcast
@@ -79,6 +91,7 @@ defmodule ExWebRTC.PeerConnection do
7991
| {:ice_candidate, ICECandidate.t()}
8092
| {:ice_connection_state_change, ice_connection_state()}
8193
| {:ice_gathering_state_change, ice_gathering_state()}
94+
| {:dtls_transport_state_change, dtls_transport_state()}
8295
| :negotiation_needed
8396
| {:signaling_state_change, signaling_state()}
8497
| {:data_channel_state_change, DataChannel.ref(), DataChannel.ready_state()}
@@ -292,6 +305,16 @@ defmodule ExWebRTC.PeerConnection do
292305
GenServer.call(peer_connection, :get_ice_gathering_state)
293306
end
294307

308+
@doc """
309+
Returns the DTLS transport state.
310+
311+
For more information, refer to the [RTCDtlsTransport: state property](https://developer.mozilla.org/en-US/docs/Web/API/RTCDtlsTransport/state).
312+
"""
313+
@spec get_dtls_transport_state(peer_connection()) :: dtls_transport_state()
314+
def get_dtls_transport_state(peer_connection) do
315+
GenServer.call(peer_connection, :get_dtls_transport_state)
316+
end
317+
295318
@doc """
296319
Returns the signaling state.
297320
@@ -577,7 +600,7 @@ defmodule ExWebRTC.PeerConnection do
577600
on_data: nil
578601
]
579602

580-
{:ok, ice_pid} = DefaultICETransport.start_link(:controlled, ice_config)
603+
{:ok, ice_pid} = DefaultICETransport.start_link(ice_config)
581604
{:ok, dtls_transport} = DTLSTransport.start_link(DefaultICETransport, ice_pid)
582605
# route data to the DTLSTransport
583606
:ok = DefaultICETransport.on_data(ice_pid, dtls_transport)
@@ -675,6 +698,11 @@ defmodule ExWebRTC.PeerConnection do
675698
{:reply, state.ice_gathering_state, state}
676699
end
677700

701+
@impl true
702+
def handle_call(:get_dtls_transport_state, _from, state) do
703+
{:reply, state.dtls_state, state}
704+
end
705+
678706
@impl true
679707
def handle_call(:get_signaling_state, _from, state) do
680708
{:reply, state.signaling_state, state}
@@ -1188,7 +1216,7 @@ defmodule ExWebRTC.PeerConnection do
11881216
timestamp: timestamp,
11891217
ice_state: ice_stats.state,
11901218
ice_gathering_state: state.ice_gathering_state,
1191-
ice_role: ice_stats.role,
1219+
ice_role: ice_stats.role || :unknown,
11921220
ice_local_ufrag: ice_stats.local_ufrag,
11931221
dtls_state: state.dtls_state,
11941222
bytes_sent: ice_stats.bytes_sent,
@@ -1359,6 +1387,8 @@ defmodule ExWebRTC.PeerConnection do
13591387

13601388
@impl true
13611389
def handle_info({:dtls_transport, _pid, {:state_change, new_dtls_state}}, state) do
1390+
notify(state.owner, {:dtls_transport_state_change, new_dtls_state})
1391+
13621392
next_conn_state = next_conn_state(state.ice_state, new_dtls_state)
13631393

13641394
state =
@@ -1716,7 +1746,13 @@ defmodule ExWebRTC.PeerConnection do
17161746
defp apply_local_description(%SessionDescription{type: type, sdp: raw_sdp}, state) do
17171747
with {:ok, next_sig_state} <- next_signaling_state(state.signaling_state, :local, type),
17181748
:ok <- check_altered(type, raw_sdp, state),
1719-
{:ok, sdp} <- parse_sdp(raw_sdp) do
1749+
{:ok, sdp} <- parse_sdp(raw_sdp),
1750+
ice_lite <- SDPUtils.get_ice_lite(sdp) do
1751+
# This has to be called before gathering candidates.
1752+
if state.ice_transport.get_role(state.ice_pid) == nil do
1753+
set_ice_role(state, :local, type, ice_lite)
1754+
end
1755+
17201756
if state.ice_gathering_state == :new do
17211757
state.ice_transport.gather_candidates(state.ice_pid)
17221758
end
@@ -1755,10 +1791,15 @@ defmodule ExWebRTC.PeerConnection do
17551791
{:ok, sdp} <- parse_sdp(raw_sdp),
17561792
:ok <- SDPUtils.ensure_valid(sdp),
17571793
{:ok, ice_creds} <- SDPUtils.get_ice_credentials(sdp),
1794+
ice_lite <- SDPUtils.get_ice_lite(sdp),
17581795
{:ok, {:fingerprint, {:sha256, peer_fingerprint}}} <- SDPUtils.get_cert_fingerprint(sdp),
17591796
{:ok, dtls_role} <- SDPUtils.get_dtls_role(sdp) do
17601797
config = Configuration.update(state.config, sdp)
17611798

1799+
if state.ice_transport.get_role(state.ice_pid) == nil do
1800+
set_ice_role(state, :remote, type, ice_lite)
1801+
end
1802+
17621803
twcc_id =
17631804
(config.video_extensions ++ config.audio_extensions)
17641805
|> Enum.find(&(&1.uri == @twcc_uri))
@@ -1922,6 +1963,23 @@ defmodule ExWebRTC.PeerConnection do
19221963
%{state | pending_remote_desc: {type, sdp}}
19231964
end
19241965

1966+
# See: https://www.w3.org/TR/webrtc/#ref-for-dfn-icerole-1
1967+
defp set_ice_role(state, :local, :offer, false) do
1968+
:ok = state.ice_transport.set_role(state.ice_pid, :controlling)
1969+
end
1970+
1971+
defp set_ice_role(state, :local, :offer, true) do
1972+
:ok = state.ice_transport.set_role(state.ice_pid, :controlled)
1973+
end
1974+
1975+
defp set_ice_role(state, :remote, :offer, true) do
1976+
:ok = state.ice_transport.set_role(state.ice_pid, :controlling)
1977+
end
1978+
1979+
defp set_ice_role(state, :remote, :offer, false) do
1980+
:ok = state.ice_transport.set_role(state.ice_pid, :controlled)
1981+
end
1982+
19251983
defp parse_sdp(raw_sdp) do
19261984
case ExSDP.parse(raw_sdp) do
19271985
{:ok, _sdp} = res -> res

lib/ex_webrtc/sdp_utils.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@ defmodule ExWebRTC.SDPUtils do
139139
|> Enum.map(fn msid -> msid.id end)
140140
end
141141

142+
@spec get_ice_lite(ExSDP.t()) :: boolean()
143+
def get_ice_lite(sdp), do: ExSDP.get_attribute(sdp, "ice-lite") != nil
144+
142145
@spec get_ice_credentials(ExSDP.t()) ::
143146
{:ok, {binary(), binary()} | nil}
144147
| {:error,

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ defmodule ExWebRTC.MixProject do
5757
defp deps do
5858
[
5959
{:ex_sdp, "~> 1.0"},
60-
{:ex_ice, "~> 0.10.0"},
60+
{:ex_ice, github: "elixir-webrtc/ex_ice"},
6161
{:ex_dtls, "~> 0.16.0"},
6262
{:ex_libsrtp, "~> 0.7.1"},
6363
{:ex_rtp, "~> 0.4.0"},

mix.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
1313
"ex_doc": {:hex, :ex_doc, "0.37.0", "970f92b39e62c460aa8a367508e938f5e4da6e2ff3eaed3f8530b25870f45471", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "b0ee7f17373948e0cf471e59c3a0ee42f3bd1171c67d91eb3626456ef9c6202c"},
1414
"ex_dtls": {:hex, :ex_dtls, "0.16.0", "3ae38025ccc77f6db573e2e391602fa9bbc02253c137d8d2d59469a66cbe806b", [:mix], [{:bundlex, "~> 1.5.3", [hex: :bundlex, repo: "hexpm", optional: false]}, {:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "2a4e30d74c6ddf95cc5b796423293c06a0da295454c3823819808ff031b4b361"},
15-
"ex_ice": {:hex, :ex_ice, "0.10.0", "cbfe28b01fea0dcb70b4c289db21e712bf22d26f63f32d6a67326d178dbdcaf5", [:mix], [{:elixir_uuid, "~> 1.0", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:ex_stun, "~> 0.2.0", [hex: :ex_stun, repo: "hexpm", optional: false]}, {:ex_turn, "~> 0.2.0", [hex: :ex_turn, repo: "hexpm", optional: false]}], "hexpm", "1490f979d7eb47115a50ebb0bae7c06d13d22932aaf43e42088474582f2af705"},
15+
"ex_ice": {:git, "https://github.com/elixir-webrtc/ex_ice.git", "b58aceee2641a35c3edfadaaef6fa9d7c48258d8", []},
1616
"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"},
1717
"ex_rtcp": {:hex, :ex_rtcp, "0.4.0", "f9e515462a9581798ff6413583a25174cfd2101c94a2ebee871cca7639886f0a", [:mix], [], "hexpm", "28956602cf210d692fcdaf3f60ca49681634e1deb28ace41246aee61ee22dc3b"},
1818
"ex_rtp": {:hex, :ex_rtp, "0.4.0", "1f1b5c1440a904706011e3afbb41741f5da309ce251cb986690ce9fd82636658", [:mix], [], "hexpm", "0f72d80d5953a62057270040f0f1ee6f955c08eeae82ac659c038001d7d5a790"},

test/ex_webrtc/dtls_transport_test.exs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ defmodule ExWebRTC.DTLSTransportTest do
2323
use GenServer
2424

2525
@impl true
26-
def start_link(_mode, config), do: GenServer.start_link(__MODULE__, config)
26+
def start_link(config), do: GenServer.start_link(__MODULE__, config)
2727

2828
@impl true
2929
def on_data(ice_pid, dst_pid), do: GenServer.call(ice_pid, {:on_data, dst_pid})
@@ -40,6 +40,11 @@ defmodule ExWebRTC.DTLSTransportTest do
4040
@impl true
4141
def gather_candidates(ice_pid), do: ice_pid
4242

43+
@impl true
44+
def get_role(ice_pid) do
45+
GenServer.call(ice_pid, :get_role)
46+
end
47+
4348
@impl true
4449
def get_local_credentials(_state), do: {:ok, "testufrag", "testpwd"}
4550

@@ -55,6 +60,11 @@ defmodule ExWebRTC.DTLSTransportTest do
5560
@impl true
5661
def set_remote_credentials(ice_pid, _ufrag, _pwd), do: ice_pid
5762

63+
@impl true
64+
def set_role(ice_pid, role) do
65+
GenServer.cast(ice_pid, {:set_role, role})
66+
end
67+
5868
@impl true
5969
def get_stats(_ice_pid), do: %{}
6070

@@ -65,13 +75,23 @@ defmodule ExWebRTC.DTLSTransportTest do
6575

6676
@impl true
6777
def init(tester: tester),
68-
do: {:ok, %{on_data_dst: nil, tester: tester}}
78+
do: {:ok, %{role: nil, on_data_dst: nil, tester: tester}}
6979

7080
@impl true
7181
def handle_call({:on_data, dst_pid}, _from, state) do
7282
{:reply, :ok, %{state | on_data_dst: dst_pid}}
7383
end
7484

85+
@impl true
86+
def handle_call(:get_role, _from, state) do
87+
{:reply, state.role, state}
88+
end
89+
90+
@impl true
91+
def handle_cast({:set_role, role}, state) do
92+
{:noreply, %{state | role: role}}
93+
end
94+
7595
@impl true
7696
def handle_cast({:send_data, data}, state) do
7797
send(state.tester, {:mock_ice, data})
@@ -86,7 +106,7 @@ defmodule ExWebRTC.DTLSTransportTest do
86106
end
87107

88108
setup do
89-
{:ok, ice_pid} = MockICETransport.start_link(:controlled, tester: self())
109+
{:ok, ice_pid} = MockICETransport.start_link(tester: self())
90110
assert {:ok, dtls} = DTLSTransport.start_link(MockICETransport, ice_pid)
91111
MockICETransport.on_data(ice_pid, dtls)
92112
assert_receive {:dtls_transport, ^dtls, {:state_change, :new}}

test/ex_webrtc/peer_connection_test.exs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,11 @@ defmodule ExWebRTC.PeerConnectionTest do
298298
:ok = PeerConnection.close(pc2)
299299
end
300300

301+
test "get_dtls_transport_state/1" do
302+
{:ok, pc} = PeerConnection.start_link()
303+
assert PeerConnection.get_dtls_transport_state(pc) == :new
304+
end
305+
301306
describe "get_local_description/1" do
302307
test "includes ICE candidates" do
303308
{:ok, pc} = PeerConnection.start()
@@ -970,7 +975,7 @@ defmodule ExWebRTC.PeerConnectionTest do
970975
assert is_binary(stats.local_certificate.fingerprint)
971976
assert is_binary(stats.local_certificate.base64_certificate)
972977

973-
assert stats.transport.ice_role in [:controlling, :controlled]
978+
assert stats.transport.ice_role == :unknown
974979
assert is_binary(stats.transport.ice_local_ufrag)
975980

976981
groups = Enum.group_by(Map.values(stats), & &1.type)

0 commit comments

Comments
 (0)