Skip to content
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
10 changes: 8 additions & 2 deletions lib/ex_webrtc/ice_transport.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@ defmodule ExWebRTC.ICETransport do
@type t() :: module()
@type state() :: :checking | :connected | :completed | :failed

@callback start_link(ExICE.ICEAgent.role(), Keyword.t()) :: {:ok, pid()}
@callback start_link(Keyword.t()) :: {:ok, pid()}
@callback on_data(pid(), pid()) :: :ok
@callback add_remote_candidate(pid(), candidate :: String.t()) :: :ok
@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 get_role(pid()) :: ExICE.ICEAgent.role() | nil
@callback restart(pid()) :: :ok
@callback send_data(pid(), binary()) :: :ok
@callback set_role(pid(), ExICE.ICEAgent.role()) :: :ok
@callback set_remote_credentials(pid(), ufrag :: binary(), pwd :: binary()) :: :ok
@callback get_stats(pid()) :: map()
@callback stop(pid()) :: :ok
Expand All @@ -28,7 +30,7 @@ defmodule ExWebRTC.DefaultICETransport do
alias ExICE.ICEAgent

@impl true
defdelegate start_link(role, opts), to: ICEAgent
defdelegate start_link(opts), to: ICEAgent
@impl true
defdelegate on_data(pid, dst_pid), to: ICEAgent
@impl true
Expand All @@ -38,6 +40,8 @@ defmodule ExWebRTC.DefaultICETransport do
@impl true
defdelegate gather_candidates(pid), to: ICEAgent
@impl true
defdelegate get_role(pid), to: ICEAgent
@impl true
defdelegate get_local_credentials(pid), to: ICEAgent
@impl true
defdelegate get_local_candidates(pid), to: ICEAgent
Expand All @@ -48,6 +52,8 @@ defmodule ExWebRTC.DefaultICETransport do
@impl true
defdelegate send_data(pid, data), to: ICEAgent
@impl true
defdelegate set_role(pid, role), to: ICEAgent
@impl true
defdelegate set_remote_credentials(pid, ufrag, pwd), to: ICEAgent
@impl true
defdelegate get_stats(pid), to: ICEAgent
Expand Down
64 changes: 61 additions & 3 deletions lib/ex_webrtc/peer_connection.ex
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ defmodule ExWebRTC.PeerConnection do
@type ice_connection_state() ::
:new | :checking | :connected | :completed | :failed | :disconnected | :closed

@typedoc """
Possible DTLS transport states.

For the exact meaning, refer to the [RTCDtlsTransport: state property](https://developer.mozilla.org/en-US/docs/Web/API/RTCDtlsTransport/state)
"""
@type dtls_transport_state() :: :new | :connecting | :connected | :failed

@typedoc """
Possible signaling states.

Expand All @@ -65,6 +72,11 @@ defmodule ExWebRTC.PeerConnection do

Most of the messages match the [RTCPeerConnection events](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection#events),
except for:
* `:dtls_transport_state_change` - traditional WebRTC implementation does not emit such event.
Instead, developer can read DTLS transport state by iterating over RTP receivers/senders, and checking their
DTLS transports states. See https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpSender/transport.
However, because Elixir WebRTC creates a single DTLS transport for all receivers and senders, there is one generic
notification for convenience and parity with other events informing about ice/signaling/connection state changes.
* `:track_muted`, `:track_ended` - these match the [MediaStreamTrack events](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack#events).
* `:data` - data received from DataChannel identified by its `ref`.
* `: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
Expand All @@ -79,6 +91,7 @@ defmodule ExWebRTC.PeerConnection do
| {:ice_candidate, ICECandidate.t()}
| {:ice_connection_state_change, ice_connection_state()}
| {:ice_gathering_state_change, ice_gathering_state()}
| {:dtls_transport_state_change, dtls_transport_state()}
| :negotiation_needed
| {:signaling_state_change, signaling_state()}
| {:data_channel_state_change, DataChannel.ref(), DataChannel.ready_state()}
Expand Down Expand Up @@ -292,6 +305,16 @@ defmodule ExWebRTC.PeerConnection do
GenServer.call(peer_connection, :get_ice_gathering_state)
end

@doc """
Returns the DTLS transport state.

For more information, refer to the [RTCDtlsTransport: state property](https://developer.mozilla.org/en-US/docs/Web/API/RTCDtlsTransport/state).
"""
@spec get_dtls_transport_state(peer_connection()) :: dtls_transport_state()
def get_dtls_transport_state(peer_connection) do
GenServer.call(peer_connection, :get_dtls_transport_state)
end

@doc """
Returns the signaling state.

Expand Down Expand Up @@ -577,7 +600,7 @@ defmodule ExWebRTC.PeerConnection do
on_data: nil
]

{:ok, ice_pid} = DefaultICETransport.start_link(:controlled, ice_config)
{:ok, ice_pid} = DefaultICETransport.start_link(ice_config)
{:ok, dtls_transport} = DTLSTransport.start_link(DefaultICETransport, ice_pid)
# route data to the DTLSTransport
:ok = DefaultICETransport.on_data(ice_pid, dtls_transport)
Expand Down Expand Up @@ -675,6 +698,11 @@ defmodule ExWebRTC.PeerConnection do
{:reply, state.ice_gathering_state, state}
end

@impl true
def handle_call(:get_dtls_transport_state, _from, state) do
{:reply, state.dtls_state, state}
end

@impl true
def handle_call(:get_signaling_state, _from, state) do
{:reply, state.signaling_state, state}
Expand Down Expand Up @@ -1188,7 +1216,7 @@ defmodule ExWebRTC.PeerConnection do
timestamp: timestamp,
ice_state: ice_stats.state,
ice_gathering_state: state.ice_gathering_state,
ice_role: ice_stats.role,
ice_role: ice_stats.role || :unknown,
ice_local_ufrag: ice_stats.local_ufrag,
dtls_state: state.dtls_state,
bytes_sent: ice_stats.bytes_sent,
Expand Down Expand Up @@ -1359,6 +1387,8 @@ defmodule ExWebRTC.PeerConnection do

@impl true
def handle_info({:dtls_transport, _pid, {:state_change, new_dtls_state}}, state) do
notify(state.owner, {:dtls_transport_state_change, new_dtls_state})

next_conn_state = next_conn_state(state.ice_state, new_dtls_state)

state =
Expand Down Expand Up @@ -1716,7 +1746,13 @@ defmodule ExWebRTC.PeerConnection do
defp apply_local_description(%SessionDescription{type: type, sdp: raw_sdp}, state) do
with {:ok, next_sig_state} <- next_signaling_state(state.signaling_state, :local, type),
:ok <- check_altered(type, raw_sdp, state),
{:ok, sdp} <- parse_sdp(raw_sdp) do
{:ok, sdp} <- parse_sdp(raw_sdp),
ice_lite <- SDPUtils.get_ice_lite(sdp) do
# This has to be called before gathering candidates.
if state.ice_transport.get_role(state.ice_pid) == nil do
set_ice_role(state, :local, type, ice_lite)
end

if state.ice_gathering_state == :new do
state.ice_transport.gather_candidates(state.ice_pid)
end
Expand Down Expand Up @@ -1755,10 +1791,15 @@ defmodule ExWebRTC.PeerConnection do
{:ok, sdp} <- parse_sdp(raw_sdp),
:ok <- SDPUtils.ensure_valid(sdp),
{:ok, ice_creds} <- SDPUtils.get_ice_credentials(sdp),
ice_lite <- SDPUtils.get_ice_lite(sdp),
{:ok, {:fingerprint, {:sha256, peer_fingerprint}}} <- SDPUtils.get_cert_fingerprint(sdp),
{:ok, dtls_role} <- SDPUtils.get_dtls_role(sdp) do
config = Configuration.update(state.config, sdp)

if state.ice_transport.get_role(state.ice_pid) == nil do
set_ice_role(state, :remote, type, ice_lite)
end

twcc_id =
(config.video_extensions ++ config.audio_extensions)
|> Enum.find(&(&1.uri == @twcc_uri))
Expand Down Expand Up @@ -1922,6 +1963,23 @@ defmodule ExWebRTC.PeerConnection do
%{state | pending_remote_desc: {type, sdp}}
end

# See: https://www.w3.org/TR/webrtc/#ref-for-dfn-icerole-1
defp set_ice_role(state, :local, :offer, false) do
:ok = state.ice_transport.set_role(state.ice_pid, :controlling)
end

defp set_ice_role(state, :local, :offer, true) do
:ok = state.ice_transport.set_role(state.ice_pid, :controlled)
end

defp set_ice_role(state, :remote, :offer, true) do
:ok = state.ice_transport.set_role(state.ice_pid, :controlling)
end

defp set_ice_role(state, :remote, :offer, false) do
:ok = state.ice_transport.set_role(state.ice_pid, :controlled)
end

defp parse_sdp(raw_sdp) do
case ExSDP.parse(raw_sdp) do
{:ok, _sdp} = res -> res
Expand Down
3 changes: 3 additions & 0 deletions lib/ex_webrtc/sdp_utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ defmodule ExWebRTC.SDPUtils do
|> Enum.map(fn msid -> msid.id end)
end

@spec get_ice_lite(ExSDP.t()) :: boolean()
def get_ice_lite(sdp), do: ExSDP.get_attribute(sdp, "ice-lite") != nil

@spec get_ice_credentials(ExSDP.t()) ::
{:ok, {binary(), binary()} | nil}
| {:error,
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ defmodule ExWebRTC.MixProject do
defp deps do
[
{:ex_sdp, "~> 1.0"},
{:ex_ice, "~> 0.10.0"},
{:ex_ice, github: "elixir-webrtc/ex_ice"},
{:ex_dtls, "~> 0.16.0"},
{:ex_libsrtp, "~> 0.7.1"},
{:ex_rtp, "~> 0.4.0"},
Expand Down
2 changes: 1 addition & 1 deletion mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
"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"},
"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"},
"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"},
"ex_ice": {:git, "https://github.com/elixir-webrtc/ex_ice.git", "b58aceee2641a35c3edfadaaef6fa9d7c48258d8", []},
"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.4.0", "f9e515462a9581798ff6413583a25174cfd2101c94a2ebee871cca7639886f0a", [:mix], [], "hexpm", "28956602cf210d692fcdaf3f60ca49681634e1deb28ace41246aee61ee22dc3b"},
"ex_rtp": {:hex, :ex_rtp, "0.4.0", "1f1b5c1440a904706011e3afbb41741f5da309ce251cb986690ce9fd82636658", [:mix], [], "hexpm", "0f72d80d5953a62057270040f0f1ee6f955c08eeae82ac659c038001d7d5a790"},
Expand Down
26 changes: 23 additions & 3 deletions test/ex_webrtc/dtls_transport_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ defmodule ExWebRTC.DTLSTransportTest do
use GenServer

@impl true
def start_link(_mode, config), do: GenServer.start_link(__MODULE__, config)
def start_link(config), do: GenServer.start_link(__MODULE__, config)

@impl true
def on_data(ice_pid, dst_pid), do: GenServer.call(ice_pid, {:on_data, dst_pid})
Expand All @@ -40,6 +40,11 @@ defmodule ExWebRTC.DTLSTransportTest do
@impl true
def gather_candidates(ice_pid), do: ice_pid

@impl true
def get_role(ice_pid) do
GenServer.call(ice_pid, :get_role)
end

@impl true
def get_local_credentials(_state), do: {:ok, "testufrag", "testpwd"}

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

@impl true
def set_role(ice_pid, role) do
GenServer.cast(ice_pid, {:set_role, role})
end

@impl true
def get_stats(_ice_pid), do: %{}

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

@impl true
def init(tester: tester),
do: {:ok, %{on_data_dst: nil, tester: tester}}
do: {:ok, %{role: nil, on_data_dst: nil, tester: tester}}

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

@impl true
def handle_call(:get_role, _from, state) do
{:reply, state.role, state}
end

@impl true
def handle_cast({:set_role, role}, state) do
{:noreply, %{state | role: role}}
end

@impl true
def handle_cast({:send_data, data}, state) do
send(state.tester, {:mock_ice, data})
Expand All @@ -86,7 +106,7 @@ defmodule ExWebRTC.DTLSTransportTest do
end

setup do
{:ok, ice_pid} = MockICETransport.start_link(:controlled, tester: self())
{:ok, ice_pid} = MockICETransport.start_link(tester: self())
assert {:ok, dtls} = DTLSTransport.start_link(MockICETransport, ice_pid)
MockICETransport.on_data(ice_pid, dtls)
assert_receive {:dtls_transport, ^dtls, {:state_change, :new}}
Expand Down
7 changes: 6 additions & 1 deletion test/ex_webrtc/peer_connection_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,11 @@ defmodule ExWebRTC.PeerConnectionTest do
:ok = PeerConnection.close(pc2)
end

test "get_dtls_transport_state/1" do
{:ok, pc} = PeerConnection.start_link()
assert PeerConnection.get_dtls_transport_state(pc) == :new
end

describe "get_local_description/1" do
test "includes ICE candidates" do
{:ok, pc} = PeerConnection.start()
Expand Down Expand Up @@ -970,7 +975,7 @@ defmodule ExWebRTC.PeerConnectionTest do
assert is_binary(stats.local_certificate.fingerprint)
assert is_binary(stats.local_certificate.base64_certificate)

assert stats.transport.ice_role in [:controlling, :controlled]
assert stats.transport.ice_role == :unknown
assert is_binary(stats.transport.ice_local_ufrag)

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