Skip to content

Commit 66fd046

Browse files
committed
Ensure codec payload types and rtp header extension ids are not updated
without the need
1 parent 61e3a04 commit 66fd046

File tree

3 files changed

+205
-33
lines changed

3 files changed

+205
-33
lines changed

lib/ex_webrtc/peer_connection/configuration.ex

Lines changed: 101 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -263,10 +263,27 @@ defmodule ExWebRTC.PeerConnection.Configuration do
263263
|> Keyword.put(:audio_extensions, Enum.map(audio_extensions, fn {_, ext} -> ext end))
264264
|> Keyword.put(:video_extensions, Enum.map(video_extensions, fn {_, ext} -> ext end))
265265
|> then(&struct(__MODULE__, &1))
266+
|> ensure_unique_payload_types()
266267
|> populate_feedbacks(feedbacks)
267268
|> add_features()
268269
end
269270

271+
defp ensure_unique_payload_types(config) do
272+
audio_pt = Enum.map(config.audio_codecs, fn codec -> codec.payload_type end)
273+
274+
if length(audio_pt) != length(Enum.uniq(audio_pt)) do
275+
raise "Payload types in audio codecs are not unique."
276+
end
277+
278+
video_pt = Enum.map(config.video_codecs, fn codec -> codec.payload_type end)
279+
280+
if length(video_pt) != length(Enum.uniq(video_pt)) do
281+
raise "Payload types in video codecs are not unique."
282+
end
283+
284+
config
285+
end
286+
270287
defp add_features(config) do
271288
%__MODULE__{features: features} = config
272289

@@ -436,21 +453,37 @@ defmodule ExWebRTC.PeerConnection.Configuration do
436453
defp do_update_extensions(extensions, sdp_extensions, free_ids) do
437454
# we replace extension ids in config to ids from the SDP
438455
# in case we have an extension in config but not in SDP, we replace
439-
# its id to some free (not present in SDP) id, so it doesn't conflict
456+
# its id only when it's occupied to some free (not present in SDP) id, so it doesn't conflict
440457
Enum.map_reduce(extensions, free_ids, fn ext, free_ids ->
441-
sdp_extensions
442-
|> Enum.find(&(&1.uri == ext.uri))
443-
|> case do
444-
nil ->
458+
case find_in_sdp_rtp_extensions(sdp_extensions, ext) do
459+
{nil, false} ->
460+
{ext, free_ids}
461+
462+
{nil, true} ->
445463
[id | rest] = free_ids
446464
{%Extmap{ext | id: id}, rest}
447465

448-
other ->
466+
{other, _id_used} ->
449467
{%Extmap{ext | id: other.id}, free_ids}
450468
end
451469
end)
452470
end
453471

472+
# Searches for rtp extension in sdp rtp extensions.
473+
# If ext is not found, id_used determines whether ext's id
474+
# is already present in sdp_extensions.
475+
# Otherwise, id_used can have any value.
476+
defp find_in_sdp_rtp_extensions(sdp_extensions, ext, id_used \\ false)
477+
defp find_in_sdp_rtp_extensions([], _ext, id_used), do: {nil, id_used}
478+
479+
defp find_in_sdp_rtp_extensions([sdp_ext | sdp_extensions], ext, id_used) do
480+
if sdp_ext.uri == ext.uri do
481+
{sdp_ext, id_used}
482+
else
483+
find_in_sdp_rtp_extensions(sdp_extensions, ext, id_used || sdp_ext.id == ext.id)
484+
end
485+
end
486+
454487
defp update_codecs(config, sdp) do
455488
%__MODULE__{audio_codecs: audio_codecs, video_codecs: video_codecs} = config
456489
sdp_codecs = SDPUtils.get_rtp_codec_parameters(sdp)
@@ -463,29 +496,27 @@ defmodule ExWebRTC.PeerConnection.Configuration do
463496
end
464497

465498
defp do_update_codecs(codecs, sdp_codecs, free_pts) do
466-
# we replace codec payload types in config to payload types from SDP
467-
# both normal codecs and rtx (we also update apt FMTP attribute in rtxs)
468-
# other codecs that are present in config but not in SDP
469-
# are also updated with values from a pool of free payload types (not present in SDP)
470-
# to make sure they don't conflict
471-
{sdp_rtxs, sdp_codecs} = Enum.split_with(sdp_codecs, &rtx?/1)
499+
# We replace codec payload types in config to payload types from SDP
500+
# both for normal codecs and rtx (we also update apt FMTP attribute in rtxs).
501+
# Other codecs that are present in config but not in SDP, and their
502+
# payload type is already present in SDP, are also updated with values
503+
# from a pool of free payload types (not present in SDP) to make sure they don't conflict
472504
{rtxs, codecs} = Enum.split_with(codecs, &rtx?/1)
473505

474506
{codecs, {free_pts, mapping}} =
475507
Enum.map_reduce(codecs, {free_pts, %{}}, fn codec, {free_pts, mapping} ->
476-
sdp_codecs
477-
|> Enum.find(
478-
&(String.downcase(&1.mime_type) == String.downcase(codec.mime_type) and
479-
&1.clock_rate == codec.clock_rate and
480-
&1.channels == codec.channels and fmtp_equal_soft?(codec, &1))
481-
)
482-
|> case do
483-
nil ->
508+
case find_in_sdp_codecs(sdp_codecs, codec) do
509+
# there is no such codec and its payload type is not used
510+
{nil, false} ->
511+
{codec, {free_pts, Map.put(mapping, codec.payload_type, codec.payload_type)}}
512+
513+
# there is no such codec, but its payload type is used
514+
{nil, true} ->
484515
[pt | rest] = free_pts
485516
new_codec = do_update_codec(codec, pt)
486517
{new_codec, {rest, Map.put(mapping, codec.payload_type, pt)}}
487518

488-
other ->
519+
{other, _pt_used} ->
489520
new_codec = do_update_codec(codec, other.payload_type)
490521
{new_codec, {free_pts, Map.put(mapping, codec.payload_type, other.payload_type)}}
491522
end
@@ -497,15 +528,18 @@ defmodule ExWebRTC.PeerConnection.Configuration do
497528
%RTPCodecParameters{rtx | sdp_fmtp_line: %FMTP{fmtp | apt: Map.fetch!(mapping, apt)}}
498529
end)
499530
|> Enum.map_reduce(free_pts, fn rtx, free_pts ->
500-
sdp_rtxs
501-
|> Enum.find(&(&1.sdp_fmtp_line.apt == rtx.sdp_fmtp_line.apt))
502-
|> case do
503-
nil ->
531+
case find_in_sdp_codecs(sdp_codecs, rtx) do
532+
# there is no such codec and its payload type is not used
533+
{nil, false} ->
534+
{rtx, free_pts}
535+
536+
# thre is no such codec, but its payload type is used
537+
{nil, true} ->
504538
[pt | rest] = free_pts
505539
rtx = do_update_codec(rtx, pt)
506540
{rtx, rest}
507541

508-
other ->
542+
{other, _pt_used} ->
509543
rtx = do_update_codec(rtx, other.payload_type)
510544
{rtx, free_pts}
511545
end
@@ -514,6 +548,38 @@ defmodule ExWebRTC.PeerConnection.Configuration do
514548
{codecs ++ rtxs, free_pts}
515549
end
516550

551+
# Searches for codec in sdp_codecs.
552+
# If codec is not found, pt_used determines whether
553+
# codec's payload type is already present in sdp_codecs.
554+
# Otherwise, pt_used can have any value.
555+
defp find_in_sdp_codecs(sdp_codecs, codec, pt_used \\ false)
556+
557+
defp find_in_sdp_codecs([], _codec, pt_used), do: {nil, pt_used}
558+
559+
defp find_in_sdp_codecs([sdp_codec | sdp_codecs], codec, pt_used) do
560+
if String.ends_with?(codec.mime_type, "/rtx") do
561+
if sdp_codec.sdp_fmtp_line != nil && sdp_codec.sdp_fmtp_line.apt == codec.sdp_fmtp_line.apt do
562+
{sdp_codec, pt_used}
563+
else
564+
find_in_sdp_codecs(
565+
sdp_codecs,
566+
codec,
567+
pt_used || sdp_codec.payload_type == codec.payload_type
568+
)
569+
end
570+
else
571+
if codec_equal_soft?(sdp_codec, codec) do
572+
{sdp_codec, pt_used}
573+
else
574+
find_in_sdp_codecs(
575+
sdp_codecs,
576+
codec,
577+
pt_used || sdp_codec.payload_type == codec.payload_type
578+
)
579+
end
580+
end
581+
end
582+
517583
defp do_update_codec(codec, new_pt) do
518584
%RTPCodecParameters{rtcp_fbs: fbs, sdp_fmtp_line: fmtp} = codec
519585
new_fbs = Enum.map(fbs, &%RTCPFeedback{&1 | pt: new_pt})
@@ -549,6 +615,7 @@ defmodule ExWebRTC.PeerConnection.Configuration do
549615
end)
550616
end
551617

618+
# soft functions does not compare payload types
552619
@doc false
553620
@spec codec_equal?(RTPCodecParameters.t(), RTPCodecParameters.t()) :: boolean()
554621
def codec_equal?(c1, c2) do
@@ -558,6 +625,14 @@ defmodule ExWebRTC.PeerConnection.Configuration do
558625
c1.channels == c2.channels and fmtp_equal?(c1, c2)
559626
end
560627

628+
@doc false
629+
@spec codec_equal_soft?(RTPCodecParameters.t(), RTPCodecParameters.t()) :: boolean()
630+
def codec_equal_soft?(c1, c2) do
631+
String.downcase(c1.mime_type) == String.downcase(c2.mime_type) and
632+
c1.clock_rate == c2.clock_rate and
633+
c1.channels == c2.channels and fmtp_equal_soft?(c1, c2)
634+
end
635+
561636
defp fmtp_equal?(%{sdp_fmtp_line: nil}, _c2), do: true
562637
defp fmtp_equal?(_c1, %{sdp_fmtp_line: nil}), do: true
563638
defp fmtp_equal?(c1, c2), do: c1.sdp_fmtp_line == c2.sdp_fmtp_line

lib/ex_webrtc/rtp_sender.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,9 @@ defmodule ExWebRTC.RTPSender do
146146
defp log_rtx_codec_change(%{rtx_codec: rtx_codec} = sender, nil, neg_codecs)
147147
when rtx_codec != nil do
148148
Logger.warning("""
149-
Unselecting RTP sender codec as it is no longer supported by the remote side.
149+
Unselecting RTP sender RTX codec as it is no longer supported by the remote side.
150150
Call set_sender_codec again passing supported codec.
151-
Codec: #{inspect(sender.codec)}
151+
Codec: #{inspect(sender.rtx_codec)}
152152
Currently negotiated codecs: #{inspect(neg_codecs)}
153153
""")
154154
end

test/ex_webrtc/peer_connection/configuration_test.exs

Lines changed: 102 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,16 +115,15 @@ defmodule ExWebRTC.PeerConnection.ConfigurationTest do
115115

116116
assert length(video_codecs) == 4
117117

118-
Enum.map(video_codecs, fn %{mime_type: mime, rtcp_fbs: rtcp_fbs} ->
118+
Enum.each(video_codecs, fn %{mime_type: mime, rtcp_fbs: rtcp_fbs} ->
119119
if String.ends_with?(mime, "/rtx") do
120120
assert rtcp_fbs == []
121121
else
122122
assert Enum.any?(rtcp_fbs, &(&1.feedback_type == :nack))
123123
end
124124
end)
125125

126-
[100, 101]
127-
|> Enum.each(fn pt ->
126+
Enum.each([100, 101], fn pt ->
128127
assert Enum.any?(
129128
video_codecs,
130129
&(String.ends_with?(&1.mime_type, "/rtx") and &1.sdp_fmtp_line.apt == pt)
@@ -161,9 +160,17 @@ defmodule ExWebRTC.PeerConnection.ConfigurationTest do
161160
end
162161
end
163162
end
163+
164+
test "with duplicated payload types" do
165+
options = [audio_codecs: [@opus_codec, @opus_codec]]
166+
assert_raise RuntimeError, fn -> Configuration.from_options!(options) end
167+
168+
options = [video_codecs: [@h264_codec, @vp8_codec]]
169+
assert_raise RuntimeError, fn -> Configuration.from_options!(options) end
170+
end
164171
end
165172

166-
describe "update!/2" do
173+
describe "update/2" do
167174
test "updates RTP header extensions" do
168175
extensions = [
169176
%{type: :all, uri: @mid_uri},
@@ -294,7 +301,7 @@ defmodule ExWebRTC.PeerConnection.ConfigurationTest do
294301
]
295302
)
296303

297-
# h264 and it's rtx both should change pt
304+
# h264 and its rtx both should change pt
298305
# vp8 should stay as it is but its rtx should change pt as it conflicts with the new h264
299306
# vp9 should just change pt
300307
sdp =
@@ -329,6 +336,96 @@ defmodule ExWebRTC.PeerConnection.ConfigurationTest do
329336
assert %{mime_type: "video/rtx", payload_type: pt, sdp_fmtp_line: %{apt: 96}} = vp8_rtx
330337
assert pt not in [100, 101, 96, 110]
331338
end
339+
340+
test "does not update anything, when there are no common codecs" do
341+
og_config = Configuration.from_options!(controlling_process: self())
342+
343+
sdp =
344+
"""
345+
m=audio 9 UDP/TLS/RTP/SAVPF 126
346+
a=rtpmap:126 newaudiocodec/48000/2
347+
m=video 9 UDP/TLS/RTP/SAVPF 127
348+
a=rtpmap:127 newvideocodec/90000
349+
"""
350+
|> ExSDP.parse!()
351+
352+
assert Enum.all?(og_config.audio_codecs, fn codec ->
353+
codec.payload_type not in [126, 127] and codec.mime_type != "audio/newaudiocodec"
354+
end)
355+
356+
assert Enum.all?(og_config.video_codecs, fn codec ->
357+
codec.payload_type not in [126, 127] and codec.mime_type != "video/newvideocodec"
358+
end)
359+
360+
assert Configuration.update(og_config, sdp) == og_config
361+
362+
# make sure that RTX codecs and RTP header extensions were also present
363+
assert og_config.audio_extensions != []
364+
assert og_config.video_extensions != []
365+
366+
assert Enum.any?(og_config.video_codecs, fn codec ->
367+
String.ends_with?(codec.mime_type, "/rtx")
368+
end)
369+
end
370+
371+
test "does not update codec payload type when FMTP does not match" do
372+
h264_codec = %RTPCodecParameters{
373+
payload_type: 98,
374+
mime_type: "video/H264",
375+
clock_rate: 90_000,
376+
sdp_fmtp_line: %FMTP{
377+
pt: 98,
378+
level_asymmetry_allowed: true,
379+
packetization_mode: 1,
380+
profile_level_id: 0x42E01F
381+
}
382+
}
383+
384+
og_config =
385+
Configuration.from_options!(
386+
controlling_process: self(),
387+
video_codecs: [h264_codec]
388+
)
389+
390+
# packetization mode in fmtp differs
391+
sdp =
392+
"""
393+
m=video 58712 UDP/TLS/RTP/SAVPF 127
394+
a=rtpmap:127 H264/90000
395+
a=rtcp-fb:127 nack
396+
a=rtcp-fb:127 nack pli
397+
a=fmtp:127 profile-level-id=42e01f;packetization-mode=0;level-asymmetry-allowed=1
398+
"""
399+
|> ExSDP.parse!()
400+
401+
assert Configuration.update(og_config, sdp) == og_config
402+
end
403+
404+
test "updates codec payload type when there is no local FMTP" do
405+
og_config =
406+
Configuration.from_options!(controlling_process: self(), video_codecs: [@h264_codec])
407+
408+
assert @h264_codec.sdp_fmtp_line == nil
409+
assert @h264_codec.payload_type != 96
410+
411+
sdp =
412+
"""
413+
m=video 58712 UDP/TLS/RTP/SAVPF 96
414+
a=rtpmap:96 H264/90000
415+
a=rtcp-fb:96 nack
416+
a=rtcp-fb:96 nack pli
417+
a=fmtp:96 profile-level-id=42e01f;packetization-mode=0;level-asymmetry-allowed=1
418+
"""
419+
|> ExSDP.parse!()
420+
421+
config = Configuration.update(og_config, sdp)
422+
423+
assert [h264, rtx] = config.video_codecs
424+
assert h264.payload_type == 96
425+
assert h264.sdp_fmtp_line == nil
426+
assert rtx.payload_type != 96
427+
assert rtx.sdp_fmtp_line.apt == 96
428+
end
332429
end
333430

334431
test "intersect_codecs/2" do

0 commit comments

Comments
 (0)