Skip to content

Commit d55453b

Browse files
committed
wip
1 parent 61e3a04 commit d55453b

File tree

3 files changed

+215
-32
lines changed

3 files changed

+215
-32
lines changed

lib/ex_webrtc/peer_connection/configuration.ex

Lines changed: 109 additions & 25 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,28 @@ 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
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
471504
{sdp_rtxs, sdp_codecs} = Enum.split_with(sdp_codecs, &rtx?/1)
472505
{rtxs, codecs} = Enum.split_with(codecs, &rtx?/1)
473506

474507
{codecs, {free_pts, mapping}} =
475508
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 ->
509+
case find_in_sdp_codecs(sdp_codecs, codec) do
510+
# there is no such codec and its payload type is not used
511+
{nil, false} ->
512+
{codec, {free_pts, Map.put(mapping, codec.payload_type, codec.payload_type)}}
513+
514+
# there is no such codec, but its payload type is used
515+
{nil, true} ->
484516
[pt | rest] = free_pts
485517
new_codec = do_update_codec(codec, pt)
486518
{new_codec, {rest, Map.put(mapping, codec.payload_type, pt)}}
487519

488-
other ->
520+
{other, _pt_used} ->
489521
new_codec = do_update_codec(codec, other.payload_type)
490522
{new_codec, {free_pts, Map.put(mapping, codec.payload_type, other.payload_type)}}
491523
end
@@ -497,15 +529,18 @@ defmodule ExWebRTC.PeerConnection.Configuration do
497529
%RTPCodecParameters{rtx | sdp_fmtp_line: %FMTP{fmtp | apt: Map.fetch!(mapping, apt)}}
498530
end)
499531
|> 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 ->
532+
case find_in_sdp_rtx_codecs(sdp_rtxs, rtx) do
533+
# there is no such codec and its payload type is not used
534+
{nil, false} ->
535+
{rtx, free_pts}
536+
537+
# thre is no such codec, but its payload type is used
538+
{nil, true} ->
504539
[pt | rest] = free_pts
505540
rtx = do_update_codec(rtx, pt)
506541
{rtx, rest}
507542

508-
other ->
543+
{other, _pt_used} ->
509544
rtx = do_update_codec(rtx, other.payload_type)
510545
{rtx, free_pts}
511546
end
@@ -514,6 +549,46 @@ defmodule ExWebRTC.PeerConnection.Configuration do
514549
{codecs ++ rtxs, free_pts}
515550
end
516551

552+
# Searches for codec in sdp_codecs.
553+
# If codec is not found, pt_used determines whether
554+
# codec's payload type is already present in sdp_codecs.
555+
# Otherwise, pt_used can have any value.
556+
defp find_in_sdp_codecs(sdp_codecs, codec, pt_used \\ false)
557+
558+
defp find_in_sdp_codecs([], _codec, pt_used), do: {nil, pt_used}
559+
560+
defp find_in_sdp_codecs([sdp_codec | sdp_codecs], codec, pt_used) do
561+
if codec_equal_soft?(sdp_codec, codec) 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+
end
571+
572+
# Searches for rtx codec in sdp_rtx_codecs.
573+
# If rtx_codec is not found, pt_used determines whether
574+
# rtx codec's payload type is already present in sdp rtx codecs.
575+
# Otherwise, pt_used can have any value.
576+
defp find_in_sdp_rtx_codecs(sdp_rtx_codecs, rtx_codec, pt_used \\ false)
577+
578+
defp find_in_sdp_rtx_codecs([], _rtx_codec, pt_used), do: {nil, pt_used}
579+
580+
defp find_in_sdp_rtx_codecs([sdp_rtx_codec | sdp_rtx_codecs], rtx_codec, pt_used) do
581+
if sdp_rtx_codec.sdp_fmtp_line.apt == rtx_codec.sdp_fmtp_line.apt do
582+
{sdp_rtx_codec, pt_used}
583+
else
584+
find_in_sdp_codecs(
585+
sdp_rtx_codecs,
586+
rtx_codec,
587+
pt_used || sdp_rtx_codec.payload_type == rtx_codec.payload_type
588+
)
589+
end
590+
end
591+
517592
defp do_update_codec(codec, new_pt) do
518593
%RTPCodecParameters{rtcp_fbs: fbs, sdp_fmtp_line: fmtp} = codec
519594
new_fbs = Enum.map(fbs, &%RTCPFeedback{&1 | pt: new_pt})
@@ -549,6 +624,7 @@ defmodule ExWebRTC.PeerConnection.Configuration do
549624
end)
550625
end
551626

627+
# soft functions does not compare payload types
552628
@doc false
553629
@spec codec_equal?(RTPCodecParameters.t(), RTPCodecParameters.t()) :: boolean()
554630
def codec_equal?(c1, c2) do
@@ -558,6 +634,14 @@ defmodule ExWebRTC.PeerConnection.Configuration do
558634
c1.channels == c2.channels and fmtp_equal?(c1, c2)
559635
end
560636

637+
@doc false
638+
@spec codec_equal_soft?(RTPCodecParameters.t(), RTPCodecParameters.t()) :: boolean()
639+
def codec_equal_soft?(c1, c2) do
640+
String.downcase(c1.mime_type) == String.downcase(c2.mime_type) and
641+
c1.clock_rate == c2.clock_rate and
642+
c1.channels == c2.channels and fmtp_equal_soft?(c1, c2)
643+
end
644+
561645
defp fmtp_equal?(%{sdp_fmtp_line: nil}, _c2), do: true
562646
defp fmtp_equal?(_c1, %{sdp_fmtp_line: nil}), do: true
563647
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: 104 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,98 @@ 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 115
346+
a=rtpmap:115 newaudiocodec/48000/2
347+
m=video 9 UDP/TLS/RTP/SAVPF 100
348+
a=rtpmap:100 newvideocodec/90000
349+
"""
350+
|> ExSDP.parse!()
351+
352+
assert Enum.all?(og_config.audio_codecs, fn codec ->
353+
codec.payload_type != 115 and codec.mime_type != "audio/newaudiocodec"
354+
end)
355+
356+
assert Enum.all?(og_config.audio_codecs, fn codec ->
357+
codec.payload_type != 100 and codec.mime_type != "audio/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 96
394+
a=rtpmap:96 H264/90000
395+
a=rtcp-fb:96 nack
396+
a=rtcp-fb:96 nack pli
397+
a=fmtp:96 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+
[_og_h264, og_rtx] = og_config.video_codecs
409+
410+
assert @h264_codec.sdp_fmtp_line == nil
411+
assert @h264_codec.payload_type != 96
412+
413+
sdp =
414+
"""
415+
m=video 58712 UDP/TLS/RTP/SAVPF 96
416+
a=rtpmap:96 H264/90000
417+
a=rtcp-fb:96 nack
418+
a=rtcp-fb:96 nack pli
419+
a=fmtp:96 profile-level-id=42e01f;packetization-mode=0;level-asymmetry-allowed=1
420+
"""
421+
|> ExSDP.parse!()
422+
423+
config = Configuration.update(og_config, sdp)
424+
425+
assert [h264, rtx] = config.video_codecs
426+
assert h264.payload_type == 96
427+
assert h264.sdp_fmtp_line == nil
428+
assert rtx.payload_type == og_rtx.payload_type
429+
assert rtx.sdp_fmtp_line.apt == 96
430+
end
332431
end
333432

334433
test "intersect_codecs/2" do

0 commit comments

Comments
 (0)