Skip to content

Commit 39cbf08

Browse files
committed
Add more renegotiation tests
1 parent 445f7fa commit 39cbf08

File tree

7 files changed

+275
-31
lines changed

7 files changed

+275
-31
lines changed

lib/ex_webrtc/peer_connection.ex

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,22 @@ defmodule ExWebRTC.PeerConnection do
8989
GenServer.call(peer_connection, {:set_local_description, description})
9090
end
9191

92+
@spec get_current_local_description(peer_connection()) :: SessionDescription.t() | nil
93+
def get_current_local_description(peer_connection) do
94+
GenServer.call(peer_connection, :get_current_local_description)
95+
end
96+
9297
@spec set_remote_description(peer_connection(), SessionDescription.t()) ::
9398
:ok | {:error, :TODO}
9499
def set_remote_description(peer_connection, description) do
95100
GenServer.call(peer_connection, {:set_remote_description, description})
96101
end
97102

103+
@spec get_current_remote_description(peer_connection()) :: SessionDescription.t() | nil
104+
def get_current_remote_description(peer_connection) do
105+
GenServer.call(peer_connection, :get_current_remote_description)
106+
end
107+
98108
@spec add_ice_candidate(peer_connection(), IceCandidate.t()) ::
99109
:ok | {:error, :TODO}
100110
def add_ice_candidate(peer_connection, candidate) do
@@ -326,6 +336,18 @@ defmodule ExWebRTC.PeerConnection do
326336
end
327337
end
328338

339+
@impl true
340+
def handle_call(:get_current_local_description, _from, state) do
341+
case state.current_local_desc do
342+
nil ->
343+
{:reply, nil, state}
344+
345+
{type, sdp} ->
346+
desc = %SessionDescription{type: type, sdp: to_string(sdp)}
347+
{:reply, desc, state}
348+
end
349+
end
350+
329351
@impl true
330352
def handle_call({:set_remote_description, desc}, _from, state) do
331353
case apply_remote_description(desc, state) do
@@ -334,6 +356,18 @@ defmodule ExWebRTC.PeerConnection do
334356
end
335357
end
336358

359+
@impl true
360+
def handle_call(:get_current_remote_description, _from, state) do
361+
case state.current_remote_desc do
362+
nil ->
363+
{:reply, nil, state}
364+
365+
{type, sdp} ->
366+
desc = %SessionDescription{type: type, sdp: to_string(sdp)}
367+
{:reply, desc, state}
368+
end
369+
end
370+
337371
@impl true
338372
def handle_call({:add_ice_candidate, _}, _from, %{current_remote_desc: nil} = state) do
339373
{:reply, {:error, :no_remote_description}, state}
@@ -691,20 +725,33 @@ defmodule ExWebRTC.PeerConnection do
691725
end)
692726

693727
# The idea is as follows:
694-
# * iterate ower answer mlines
695-
# * if there is transceiver's mline that should replace
696-
# answer's mline, do it (i.e. recycle free mline)
697-
# * if there is no transceiver's mline, just rewrite
698-
# answer's mline. This is to preserve the same number of mlines
699-
# between subsequent offer/anser exchanges
700-
# * at the end, add remaining transceiver mlines
728+
# * Iterate ower current local mlines
729+
# * If there is transceiver's mline that should replace
730+
# mline from the last offer/answer, do it (i.e. recycle free mline)
731+
# * If there is no transceiver's mline, just rewrite
732+
# mline from the offer/answer respecting its port number i.e. whether
733+
# it is rejected or not.
734+
# This is to preserve the same number of mlines
735+
# between subsequent offer/anser exchanges.
736+
# * At the end, add remaining transceiver mlines
737+
{_, current_local_desc} = state.current_local_desc
738+
{_, current_remote_desc} = state.current_remote_desc
739+
701740
final_mlines =
702-
last_answer.media
741+
current_local_desc.media
703742
|> Stream.with_index()
704-
|> Enum.map(fn {answer_mline, idx} ->
743+
|> Enum.map(fn {local_mline, idx} ->
705744
case Enum.find(transceivers, &(&1.mline_idx == idx)) do
706-
nil -> answer_mline
707-
tr -> RTPTransceiver.to_offer_mline(tr, opts)
745+
nil when current_remote_desc == nil ->
746+
local_mline
747+
748+
nil when current_remote_desc != nil ->
749+
remote_mline = Enum.at(current_remote_desc.media, idx)
750+
# credo:disable-for-next-line Credo.Check.Refactor.Nesting
751+
if remote_mline.port == 0, do: %{local_mline | port: 0}, else: local_mline
752+
753+
tr ->
754+
RTPTransceiver.to_offer_mline(tr, opts)
708755
end
709756
end)
710757

lib/ex_webrtc/rtp_transceiver.ex

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -121,15 +121,28 @@ defmodule ExWebRTC.RTPTransceiver do
121121
@doc false
122122
@spec to_answer_mline(t(), ExSDP.Media.t(), Keyword.t()) :: ExSDP.Media.t()
123123
def to_answer_mline(transceiver, mline, opts) do
124-
if transceiver.codecs == [] or transceiver.stopping == true or transceiver.stopped == true do
125-
# reject mline and skip further processing
126-
# see RFC 8829 sec. 5.3.1 and RFC 3264 sec. 6
127-
%ExSDP.Media{mline | port: 0}
128-
else
129-
offered_direction = SDPUtils.get_media_direction(mline)
130-
direction = get_direction(offered_direction, transceiver.direction)
131-
opts = Keyword.put(opts, :direction, direction)
132-
to_mline(transceiver, opts)
124+
offered_direction = SDPUtils.get_media_direction(mline)
125+
direction = get_direction(offered_direction, transceiver.direction)
126+
opts = Keyword.put(opts, :direction, direction)
127+
128+
# Reject mline. See RFC 8829 sec. 5.3.1 and RFC 3264 sec. 6.
129+
# We could reject earlier (as RFC suggests) but we generate
130+
# answer mline at first to have consistent fingerprint, ice_ufrag and
131+
# ice_pwd values across mlines.
132+
cond do
133+
transceiver.codecs == [] ->
134+
# there has to be at least one format so take it from the offer
135+
codecs = SDPUtils.get_rtp_codec_parameters(mline)
136+
transceiver = %__MODULE__{transceiver | codecs: codecs}
137+
mline = to_mline(transceiver, opts)
138+
%ExSDP.Media{mline | port: 0}
139+
140+
transceiver.stopping == true or transceiver.stopped == true ->
141+
mline = to_mline(transceiver, opts)
142+
%ExSDP.Media{mline | port: 0}
143+
144+
true ->
145+
to_mline(transceiver, opts)
133146
end
134147
end
135148

lib/ex_webrtc/sdp_utils.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ defmodule ExWebRTC.SDPUtils do
265265
def find_free_mline_idx(sdp, indices) do
266266
sdp.media
267267
|> Stream.with_index()
268-
|> Enum.find(fn {mline, idx} -> mline.port == 0 and idx not in indices end)
268+
|> Enum.find_index(fn {mline, idx} -> mline.port == 0 and idx not in indices end)
269269
end
270270

271271
@spec is_rejected(ExSDP.Media.t()) :: boolean()

mix.exs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ defmodule ExWebRTC.MixProject do
99
app: :ex_webrtc,
1010
version: @version,
1111
elixir: "~> 1.15",
12+
elixirc_paths: elixirc_paths(Mix.env()),
1213
start_permanent: Mix.env() == :prod,
1314
description: "Implementation of WebRTC",
1415
package: package(),
@@ -36,6 +37,9 @@ defmodule ExWebRTC.MixProject do
3637
]
3738
end
3839

40+
defp elixirc_paths(:test), do: ["lib", "test/support"]
41+
defp elixirc_paths(_env), do: ["lib"]
42+
3943
def package do
4044
[
4145
licenses: ["Apache-2.0"],

test/ex_webrtc/peer_connection_test.exs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
defmodule ExWebRTC.PeerConnectionTest do
22
use ExUnit.Case, async: true
33

4+
import ExWebRTC.Support.TestUtils
5+
46
alias ExWebRTC.{
57
RTPTransceiver,
68
RTPSender,
@@ -531,6 +533,12 @@ defmodule ExWebRTC.PeerConnectionTest do
531533
:ok = PeerConnection.set_remote_description(pc1, answer)
532534
assert [] == PeerConnection.get_transceivers(pc1)
533535
end
536+
537+
test "with invalid transceiver id" do
538+
{:ok, pc} = PeerConnection.start_link()
539+
{:ok, tr} = PeerConnection.add_transceiver(pc, :audio)
540+
assert {:error, :invalid_transceiver_id} == PeerConnection.stop_transceiver(pc, tr.id + 1)
541+
end
534542
end
535543

536544
describe "add_track/2" do
@@ -878,14 +886,4 @@ defmodule ExWebRTC.PeerConnectionTest do
878886
assert [%RTPTransceiver{direction: :inactive, current_direction: :inactive}] =
879887
PeerConnection.get_transceivers(pc2)
880888
end
881-
882-
defp negotiate(pc1, pc2) do
883-
{:ok, offer} = PeerConnection.create_offer(pc1)
884-
:ok = PeerConnection.set_local_description(pc1, offer)
885-
:ok = PeerConnection.set_remote_description(pc2, offer)
886-
{:ok, answer} = PeerConnection.create_answer(pc2)
887-
:ok = PeerConnection.set_local_description(pc2, answer)
888-
:ok = PeerConnection.set_remote_description(pc1, answer)
889-
:ok
890-
end
891889
end
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
defmodule ExWebRTC.RenegotiationTest do
2+
use ExUnit.Case, async: true
3+
4+
import ExWebRTC.Support.TestUtils
5+
6+
alias ExWebRTC.{PeerConnection, RTPTransceiver}
7+
8+
test "stop two and add one with different kind" do
9+
# 1. add audio and video transceiver
10+
# 2. stop both
11+
# 3. add video
12+
# 4. the output should be video (9), video (0)
13+
{:ok, pc1} = PeerConnection.start_link()
14+
{:ok, pc2} = PeerConnection.start_link()
15+
16+
{:ok, pc1_tr1} = PeerConnection.add_transceiver(pc1, :audio)
17+
{:ok, pc1_tr2} = PeerConnection.add_transceiver(pc1, :video)
18+
19+
:ok = negotiate(pc1, pc2)
20+
21+
[pc2_tr1, pc2_tr2] = PeerConnection.get_transceivers(pc2)
22+
23+
:ok = PeerConnection.stop_transceiver(pc1, pc1_tr1.id)
24+
:ok = PeerConnection.stop_transceiver(pc1, pc1_tr2.id)
25+
26+
:ok = negotiate(pc1, pc2)
27+
28+
{:ok, pc1_tr3} = PeerConnection.add_transceiver(pc1, :video)
29+
30+
# should reuse the first mline even though it's of kind audio
31+
{:ok, offer} = PeerConnection.create_offer(pc1)
32+
sdp = ExSDP.parse!(offer.sdp)
33+
assert [%{type: :video, port: 9}, %{type: :video, port: 0}] = sdp.media
34+
35+
assert :ok = continue_negotiation(pc1, pc2)
36+
37+
pc1_tr3_id = pc1_tr3.id
38+
39+
[
40+
%RTPTransceiver{
41+
id: ^pc1_tr3_id,
42+
kind: :video,
43+
current_direction: :sendonly,
44+
direction: :sendrecv,
45+
stopping: false,
46+
stopped: false
47+
}
48+
] = PeerConnection.get_transceivers(pc1)
49+
50+
[
51+
%RTPTransceiver{
52+
kind: :video,
53+
current_direction: :recvonly,
54+
direction: :recvonly,
55+
stopped: false,
56+
stopping: false
57+
} = tr
58+
] = PeerConnection.get_transceivers(pc2)
59+
60+
assert tr.id != pc2_tr1.id
61+
assert tr.id != pc2_tr2.id
62+
end
63+
64+
test "stop one and add two with switched kinds" do
65+
# 1. add audio and video transceiver
66+
# 2. stop audio transceiver
67+
# 3. add video and audio
68+
# 4. the output should be video (9), video (9), audio (9)
69+
{:ok, pc1} = PeerConnection.start_link()
70+
{:ok, pc2} = PeerConnection.start_link()
71+
72+
{:ok, pc1_tr1} = PeerConnection.add_transceiver(pc1, :audio)
73+
{:ok, pc1_tr2} = PeerConnection.add_transceiver(pc1, :video)
74+
75+
:ok = negotiate(pc1, pc2)
76+
77+
[pc2_tr1, pc2_tr2] = PeerConnection.get_transceivers(pc2)
78+
79+
:ok = PeerConnection.stop_transceiver(pc1, pc1_tr1.id)
80+
81+
:ok = negotiate(pc1, pc2)
82+
83+
{:ok, pc1_tr3} = PeerConnection.add_transceiver(pc1, :video)
84+
{:ok, pc1_tr4} = PeerConnection.add_transceiver(pc1, :audio)
85+
86+
{:ok, offer} = PeerConnection.create_offer(pc1)
87+
sdp = ExSDP.parse!(offer.sdp)
88+
89+
assert [%{type: :video, port: 9}, %{type: :video, port: 9}, %{type: :audio, port: 9}] =
90+
sdp.media
91+
92+
assert :ok = continue_negotiation(pc1, pc2)
93+
94+
pc1_tr2_id = pc1_tr2.id
95+
pc1_tr3_id = pc1_tr3.id
96+
pc1_tr4_id = pc1_tr4.id
97+
98+
[
99+
%RTPTransceiver{
100+
id: ^pc1_tr2_id,
101+
kind: :video,
102+
current_direction: :sendonly,
103+
direction: :sendrecv,
104+
stopping: false,
105+
stopped: false
106+
},
107+
%RTPTransceiver{
108+
id: ^pc1_tr3_id,
109+
kind: :video,
110+
current_direction: :sendonly,
111+
direction: :sendrecv,
112+
stopping: false,
113+
stopped: false
114+
},
115+
%RTPTransceiver{
116+
id: ^pc1_tr4_id,
117+
kind: :audio,
118+
current_direction: :sendonly,
119+
direction: :sendrecv,
120+
stopping: false,
121+
stopped: false
122+
}
123+
] = PeerConnection.get_transceivers(pc1)
124+
125+
pc2_tr2_id = pc2_tr2.id
126+
127+
[
128+
%RTPTransceiver{
129+
id: ^pc2_tr2_id,
130+
kind: :video,
131+
current_direction: :recvonly,
132+
direction: :recvonly,
133+
stopped: false,
134+
stopping: false
135+
},
136+
%RTPTransceiver{
137+
kind: :video,
138+
current_direction: :recvonly,
139+
direction: :recvonly,
140+
stopped: false,
141+
stopping: false
142+
} = tr2,
143+
%RTPTransceiver{
144+
kind: :audio,
145+
current_direction: :recvonly,
146+
direction: :recvonly,
147+
stopped: false,
148+
stopping: false
149+
} = tr3
150+
] = PeerConnection.get_transceivers(pc2)
151+
152+
# make sure we didn't reuse stopped transceiver
153+
assert tr2.id != pc2_tr1.id
154+
assert tr3.id != pc2_tr1.id
155+
end
156+
157+
defp continue_negotiation(pc1, pc2) do
158+
{:ok, offer} = PeerConnection.create_offer(pc1)
159+
:ok = PeerConnection.set_local_description(pc1, offer)
160+
:ok = PeerConnection.set_remote_description(pc2, offer)
161+
{:ok, answer} = PeerConnection.create_answer(pc2)
162+
:ok = PeerConnection.set_local_description(pc2, answer)
163+
:ok = PeerConnection.set_remote_description(pc1, answer)
164+
:ok
165+
end
166+
end

test/support/test_utils.ex

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
defmodule ExWebRTC.Support.TestUtils do
2+
@moduledoc false
3+
4+
alias ExWebRTC.PeerConnection
5+
6+
@spec negotiate(PeerConnection.peer_connection(), PeerConnection.peer_connection()) :: :ok
7+
def negotiate(pc1, pc2) do
8+
{:ok, offer} = PeerConnection.create_offer(pc1)
9+
:ok = PeerConnection.set_local_description(pc1, offer)
10+
:ok = PeerConnection.set_remote_description(pc2, offer)
11+
{:ok, answer} = PeerConnection.create_answer(pc2)
12+
:ok = PeerConnection.set_local_description(pc2, answer)
13+
:ok = PeerConnection.set_remote_description(pc1, answer)
14+
:ok
15+
end
16+
end

0 commit comments

Comments
 (0)