diff --git a/lib/ex_webrtc/peer_connection.ex b/lib/ex_webrtc/peer_connection.ex index a0be88ac..1bc6494e 100644 --- a/lib/ex_webrtc/peer_connection.ex +++ b/lib/ex_webrtc/peer_connection.ex @@ -280,15 +280,11 @@ defmodule ExWebRTC.PeerConnection do defp apply_remote_description(_type, sdp, state) do # TODO apply steps listed in RFC 8829 5.10 - media = hd(sdp.media) - 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, {ice_ufrag, ice_pwd}} <- SDPUtils.get_ice_credentials(sdp), {:ok, new_transceivers} <- update_transceivers(state.transceivers, sdp) do - :ok = ICEAgent.set_remote_credentials(state.ice_agent, ufrag, pwd) + :ok = ICEAgent.set_remote_credentials(state.ice_agent, ice_ufrag, ice_pwd) :ok = ICEAgent.gather_candidates(state.ice_agent) new_remote_tracks = @@ -306,9 +302,7 @@ defmodule ExWebRTC.PeerConnection do {:ok, %{state | current_remote_desc: sdp, transceivers: new_transceivers}} else - {:ice_ufrag, nil} -> {:error, :missing_ice_ufrag} - {:ice_pwd, nil} -> {:error, :missing_ice_pwd} - other -> other + {:error, _reason} = error -> error end end diff --git a/lib/ex_webrtc/sdp_utils.ex b/lib/ex_webrtc/sdp_utils.ex index 2ba02ae8..14785a54 100644 --- a/lib/ex_webrtc/sdp_utils.ex +++ b/lib/ex_webrtc/sdp_utils.ex @@ -56,4 +56,85 @@ defmodule ExWebRTC.SDPUtils do {:error, :multiple_bundle_groups} end end + + @spec get_ice_credentials(ExSDP.t()) :: + {:ok, binary(), binary()} + | {:error, + :missing_ice_pwd + | :missing_ice_ufrag + | :missing_ice_credentials + | :conflicting_ice_credentials} + def get_ice_credentials(sdp) do + session_creds = do_get_ice_credentials(sdp) + mline_creds = Enum.map(sdp.media, fn mline -> do_get_ice_credentials(mline) end) + + case {session_creds, mline_creds} do + {{nil, nil}, []} -> + {:error, :missing_ice_credentials} + + {session_creds, []} -> + {:ok, session_creds} + + {{nil, nil}, mline_creds} -> + with :ok <- ensure_ice_credentials_present(mline_creds), + :ok <- ensure_ice_credentials_unique(mline_creds) do + {:ok, List.first(mline_creds)} + end + + {{s_ufrag, s_pwd} = session_creds, mline_creds} -> + creds = + Enum.map([session_creds | mline_creds], fn + {nil, nil} -> session_creds + {nil, pwd} -> {s_ufrag, pwd} + {ufrag, nil} -> {ufrag, s_pwd} + other -> other + end) + + case ensure_ice_credentials_unique(creds) do + :ok -> {:ok, List.first(creds)} + error -> error + end + end + end + + defp do_get_ice_credentials(sdp_or_mline) do + ice_ufrag = + case ExSDP.Media.get_attribute(sdp_or_mline, :ice_ufrag) do + {:ice_ufrag, ice_ufrag} -> ice_ufrag + nil -> nil + end + + ice_pwd = + case ExSDP.Media.get_attribute(sdp_or_mline, :ice_pwd) do + {:ice_pwd, ice_pwd} -> ice_pwd + nil -> nil + end + + {ice_ufrag, ice_pwd} + end + + defp ensure_ice_credentials_present(creds) do + creds + |> Enum.find(fn {ice_ufrag, ice_pwd} -> ice_ufrag == nil or ice_pwd == nil end) + |> case do + {nil, nil} -> + {:error, :missing_ice_credentials} + + {nil, _} -> + {:error, :missing_ice_ufrag} + + {_, nil} -> + {:error, :missing_ice_pwd} + + nil -> + :ok + end + end + + defp ensure_ice_credentials_unique(creds) do + case Enum.uniq(creds) do + [_] -> :ok + _ -> {:error, :conflicting_ice_credentials} + end + end end diff --git a/test/peer_connection_test.exs b/test/peer_connection_test.exs index 6ec60c55..5b7051f0 100644 --- a/test/peer_connection_test.exs +++ b/test/peer_connection_test.exs @@ -81,6 +81,12 @@ defmodule ExWebRTC.PeerConnectionTest do a=ssrc:184440407 msid:- 1259ea70-c6b7-445a-9c20-49cec7433ccb """ + @audio_mline ExSDP.Media.new("audio", 9, "UDP/TLS/RTP/SAVPF", [108]) + |> ExSDP.Media.add_attributes(mid: "0", ice_ufrag: "someufrag", ice_pwd: "somepwd") + + @video_mline ExSDP.Media.new("video", 9, "UDP/TLS/RTP/SAVPF", [96]) + |> ExSDP.Media.add_attributes(mid: "1", ice_ufrag: "someufrag", ice_pwd: "somepwd") + test "track notification" do {:ok, pc} = PeerConnection.start_link() @@ -96,46 +102,121 @@ defmodule ExWebRTC.PeerConnectionTest do refute_receive {:ex_webrtc, ^pc, {:track, %MediaStreamTrack{}}} end - test "set_remote_description/2" do - {:ok, pc} = PeerConnection.start_link() - - raw_sdp = ExSDP.new() - - audio_mline = - ExSDP.Media.new("audio", 9, "UDP/TLS/RTP/SAVPF", [108]) - |> ExSDP.Media.add_attributes(mid: "0", ice_ufrag: "someufrag", ice_pwd: "somepwd") - - video_mline = - ExSDP.Media.new("video", 9, "UDP/TLS/RTP/SAVPF", [96]) - |> ExSDP.Media.add_attributes(mid: "1", ice_ufrag: "someufrag", ice_pwd: "somepwd") - - sdp = ExSDP.add_media(raw_sdp, audio_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(audio_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) - - sdp = - raw_sdp - |> ExSDP.add_attribute(%ExSDP.Attribute.Group{semantics: "BUNDLE", mids: [0]}) - |> ExSDP.add_media(audio_mline) - - offer = %SessionDescription{type: :offer, sdp: to_string(sdp)} - assert :ok == PeerConnection.set_remote_description(pc, offer) - - sdp = ExSDP.add_media(sdp, video_mline) |> to_string() - - offer = %SessionDescription{type: :offer, sdp: sdp} - - assert {:error, :non_exhaustive_bundle_group} == - PeerConnection.set_remote_description(pc, offer) + describe "set_remote_description/2" do + test "MID" do + {:ok, pc} = PeerConnection.start_link() + + raw_sdp = ExSDP.new() + + mline = ExSDP.Media.add_attribute(@audio_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 + + test "BUNDLE group" do + {:ok, pc} = PeerConnection.start_link() + + sdp = ExSDP.add_media(ExSDP.new(), [@audio_mline, @video_mline]) + + [ + {nil, {:error, :missing_bundle_group}}, + {%ExSDP.Attribute.Group{semantics: "BUNDLE", mids: [0]}, + {:error, :non_exhaustive_bundle_group}}, + {%ExSDP.Attribute.Group{semantics: "BUNDLE", mids: [0, 1]}, :ok} + ] + |> Enum.each(fn {bundle_group, expected_result} -> + sdp = ExSDP.add_attribute(sdp, bundle_group) |> to_string() + offer = %SessionDescription{type: :offer, sdp: sdp} + assert expected_result == PeerConnection.set_remote_description(pc, offer) + end) + end + + test "ICE credentials" do + {:ok, pc} = PeerConnection.start_link() + + raw_sdp = ExSDP.new() + + [ + {{nil, nil}, {"someufrag", "somepwd"}, {"someufrag", "somepwd"}, :ok}, + {{"someufrag", "somepwd"}, {"someufrag", "somepwd"}, {nil, nil}, :ok}, + {{"someufrag", "somepwd"}, {nil, nil}, {nil, nil}, :ok}, + {{"someufrag", "somepwd"}, {"someufrag", nil}, {nil, "somepwd"}, :ok}, + {{nil, nil}, {"someufrag", "somepwd"}, {nil, nil}, {:error, :missing_ice_credentials}}, + {{nil, nil}, {"someufrag", "somepwd"}, {"someufrag", nil}, {:error, :missing_ice_pwd}}, + {{nil, nil}, {"someufrag", "somepwd"}, {nil, "somepwd"}, {:error, :missing_ice_ufrag}}, + {{nil, nil}, {nil, nil}, {nil, nil}, {:error, :missing_ice_credentials}}, + {{nil, nil}, {"someufrag", "somepwd"}, {"someufrag", "someotherpwd"}, + {:error, :conflicting_ice_credentials}} + ] + |> Enum.each(fn {{s_ufrag, s_pwd}, {a_ufrag, a_pwd}, {v_ufrag, v_pwd}, expected_result} -> + audio_mline = + ExSDP.Media.new("audio", 9, "UDP/TLS/RTP/SAVPF", [108]) + |> ExSDP.Media.add_attributes(mid: "0") + + video_mline = + ExSDP.Media.new("video", 9, "UDP/TLS/RTP/SAVPF", [96]) + |> ExSDP.Media.add_attributes(mid: "1") + + audio_mline = + if a_ufrag do + ExSDP.Media.add_attribute(audio_mline, {:ice_ufrag, a_ufrag}) + else + audio_mline + end + + audio_mline = + if a_pwd do + ExSDP.Media.add_attribute(audio_mline, {:ice_pwd, a_pwd}) + else + audio_mline + end + + video_mline = + if v_ufrag do + ExSDP.Media.add_attribute(video_mline, {:ice_ufrag, v_ufrag}) + else + video_mline + end + + video_mline = + if v_pwd do + ExSDP.Media.add_attribute(video_mline, {:ice_pwd, v_pwd}) + else + video_mline + end + + sdp = + ExSDP.add_attribute(raw_sdp, %ExSDP.Attribute.Group{semantics: "BUNDLE", mids: [0, 1]}) + + sdp = + if s_ufrag do + ExSDP.add_attribute(sdp, {:ice_ufrag, s_ufrag}) + else + sdp + end + + sdp = + if s_pwd do + ExSDP.add_attribute(sdp, {:ice_pwd, s_pwd}) + else + sdp + end + + sdp = + sdp + |> ExSDP.add_media([audio_mline, video_mline]) + |> to_string() + + offer = %SessionDescription{type: :offer, sdp: sdp} + + assert expected_result == PeerConnection.set_remote_description(pc, offer) + end) + end end end