Skip to content

Commit 053d520

Browse files
authored
Add set_sender_codec (#190)
1 parent 0c930f2 commit 053d520

File tree

11 files changed

+821
-287
lines changed

11 files changed

+821
-287
lines changed

lib/ex_webrtc/peer_connection.ex

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ defmodule ExWebRTC.PeerConnection do
99

1010
require Logger
1111

12+
alias ExWebRTC.RTPCodecParameters
1213
alias __MODULE__.{Configuration, Demuxer, TWCCRecorder}
1314

1415
alias ExWebRTC.{
@@ -152,9 +153,67 @@ defmodule ExWebRTC.PeerConnection do
152153
GenServer.call(peer_connection, :get_configuration)
153154
end
154155

156+
@doc """
157+
Sets the codec that will be used for sending RTP packets.
158+
159+
`send_rtp/4` overrides some of the RTP packet fields.
160+
In particular, when multiple codecs are negotiated, `send_rtp/4` will use
161+
payload type of the most preffered by the remote side codec (i.e. the first
162+
one from the list of codecs in the remote description).
163+
164+
Use this function if you want to select, which codec (hence payload type)
165+
should be used for sending.
166+
167+
Once the first RTP packet is sent (via `send_rtp/4`), `set_sender_codec/3`
168+
can only be called with a codec with the same clock rate.
169+
170+
Although very unlikely, keep in mind that after renegotiation,
171+
the selected codec may no longer be supported by the remote side and you might
172+
need to call this function again, passing a new codec.
173+
174+
To check available codecs you can use `get_transceivers/1`:
175+
176+
```
177+
{:ok, pc} = PeerConnection.start_link()
178+
{:ok, rtp_sender} = PeerConnection.add_track(MediaStreamTrack.new(:video))
179+
180+
# exchange SDP with the remote side
181+
# {:ok, offer} = PeerConnection.create_offer(pc)
182+
# ...
183+
184+
tr =
185+
pc
186+
|> PeerConnection.get_transceivers()
187+
|> Enum.find(fn tr -> tr.sender.id == rtp_sender.id end)
188+
189+
dbg(tr.codecs) # list of supported codecs both for sending and receiving
190+
191+
# e.g. always prefer h264 over vp8
192+
h264 = Enum.find(tr.codecs, fn codec -> codec.mime_type == "video/H264" end)
193+
vp8 = Enum.find(tr.codecs, fn codec -> codec.mime_type == "video/VP8" end)
194+
195+
:ok = PeerConnection.set_sender_codec(pc, rtp_sender.id, h264 || vp8)
196+
```
197+
198+
This function can only be called once the first negotiation passes.
199+
"""
200+
@spec set_sender_codec(peer_connection(), RTPSender.id(), RTPCodecParameters.t()) ::
201+
:ok | {:error, term()}
202+
def set_sender_codec(peer_connection, sender_id, codec) do
203+
GenServer.call(peer_connection, {:set_sender_codec, sender_id, codec})
204+
end
205+
155206
@doc """
156207
Sends an RTP packet to the remote peer using the track specified by the `track_id`.
157208
209+
The following fields of the RTP packet will be overwritten by this function:
210+
* payload type
211+
* ssrc
212+
* rtp header extensions
213+
214+
If you negotiated multiple codecs (hence payload types) and you want to choose,
215+
which one should be used, see `set_sender_codec/3`.
216+
158217
Options:
159218
* `rtx?` - send the packet as if it was retransmitted (use SSRC and payload type specific to RTX)
160219
"""
@@ -576,6 +635,31 @@ defmodule ExWebRTC.PeerConnection do
576635
{:reply, state.config, state}
577636
end
578637

638+
@impl true
639+
def handle_call({:set_sender_codec, sender_id, codec}, _from, state) do
640+
state.transceivers
641+
|> Enum.with_index()
642+
|> Enum.find(fn {tr, _idx} -> tr.sender.id == sender_id end)
643+
|> case do
644+
{tr, idx} when tr.direction in [:sendrecv, :sendonly] ->
645+
case RTPTransceiver.set_sender_codec(tr, codec) do
646+
{:ok, tr} ->
647+
transceivers = List.replace_at(state.transceivers, idx, tr)
648+
state = %{state | transceivers: transceivers}
649+
{:reply, :ok, state}
650+
651+
{:error, _reason} = error ->
652+
{:reply, error, state}
653+
end
654+
655+
{_tr, _idx} ->
656+
{:reply, {:error, :invalid_transceiver_direction}, state}
657+
658+
nil ->
659+
{:reply, {:error, :invalid_sender_id}, state}
660+
end
661+
end
662+
579663
@impl true
580664
def handle_call(:get_connection_state, _from, state) do
581665
{:reply, state.conn_state, state}
@@ -1135,7 +1219,8 @@ defmodule ExWebRTC.PeerConnection do
11351219
end
11361220

11371221
{packet, tr} = RTPTransceiver.send_packet(tr, packet, rtx?)
1138-
:ok = DTLSTransport.send_rtp(state.dtls_transport, packet)
1222+
1223+
if packet != <<>>, do: :ok = DTLSTransport.send_rtp(state.dtls_transport, packet)
11391224

11401225
transceivers = List.replace_at(state.transceivers, idx, tr)
11411226
state = %{state | transceivers: transceivers}

lib/ex_webrtc/peer_connection/configuration.ex

Lines changed: 140 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,22 @@ defmodule ExWebRTC.PeerConnection.Configuration do
3535
clock_rate: 90_000,
3636
sdp_fmtp_line: %FMTP{
3737
pt: 98,
38-
level_asymmetry_allowed: 1,
38+
level_asymmetry_allowed: true,
3939
packetization_mode: 0,
4040
profile_level_id: 0x42E01F
4141
}
4242
},
43+
%RTPCodecParameters{
44+
payload_type: 99,
45+
mime_type: "video/H264",
46+
clock_rate: 90_000,
47+
sdp_fmtp_line: %FMTP{
48+
pt: 99,
49+
level_asymmetry_allowed: true,
50+
packetization_mode: 1,
51+
profile_level_id: 0x42E01F
52+
}
53+
},
4354
%RTPCodecParameters{
4455
payload_type: 45,
4556
mime_type: "video/AV1",
@@ -252,10 +263,27 @@ defmodule ExWebRTC.PeerConnection.Configuration do
252263
|> Keyword.put(:audio_extensions, Enum.map(audio_extensions, fn {_, ext} -> ext end))
253264
|> Keyword.put(:video_extensions, Enum.map(video_extensions, fn {_, ext} -> ext end))
254265
|> then(&struct(__MODULE__, &1))
266+
|> ensure_unique_payload_types()
255267
|> populate_feedbacks(feedbacks)
256268
|> add_features()
257269
end
258270

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+
259287
defp add_features(config) do
260288
%__MODULE__{features: features} = config
261289

@@ -405,6 +433,9 @@ defmodule ExWebRTC.PeerConnection.Configuration do
405433
config
406434
|> update_extensions(sdp)
407435
|> update_codecs(sdp)
436+
# if update went wrong (there are duplicates in payload types),
437+
# we should never continue as this may lead to hard to debug errors
438+
|> ensure_unique_payload_types()
408439
end
409440

410441
defp update_extensions(config, sdp) do
@@ -425,25 +456,41 @@ defmodule ExWebRTC.PeerConnection.Configuration do
425456
defp do_update_extensions(extensions, sdp_extensions, free_ids) do
426457
# we replace extension ids in config to ids from the SDP
427458
# in case we have an extension in config but not in SDP, we replace
428-
# its id to some free (not present in SDP) id, so it doesn't conflict
459+
# its id only when it's occupied to some free (not present in SDP) id, so it doesn't conflict
429460
Enum.map_reduce(extensions, free_ids, fn ext, free_ids ->
430-
sdp_extensions
431-
|> Enum.find(&(&1.uri == ext.uri))
432-
|> case do
433-
nil ->
461+
case find_in_sdp_rtp_extensions(sdp_extensions, ext) do
462+
{nil, false} ->
463+
{ext, free_ids}
464+
465+
{nil, true} ->
434466
[id | rest] = free_ids
435467
{%Extmap{ext | id: id}, rest}
436468

437-
other ->
469+
{other, _id_used} ->
438470
{%Extmap{ext | id: other.id}, free_ids}
439471
end
440472
end)
441473
end
442474

475+
# Searches for rtp extension in sdp rtp extensions.
476+
# If ext is not found, id_used determines whether ext's id
477+
# is already present in sdp_extensions.
478+
# Otherwise, id_used can have any value.
479+
defp find_in_sdp_rtp_extensions(sdp_extensions, ext, id_used \\ false)
480+
defp find_in_sdp_rtp_extensions([], _ext, id_used), do: {nil, id_used}
481+
482+
defp find_in_sdp_rtp_extensions([sdp_ext | sdp_extensions], ext, id_used) do
483+
if sdp_ext.uri == ext.uri do
484+
{sdp_ext, id_used}
485+
else
486+
find_in_sdp_rtp_extensions(sdp_extensions, ext, id_used || sdp_ext.id == ext.id)
487+
end
488+
end
489+
443490
defp update_codecs(config, sdp) do
444491
%__MODULE__{audio_codecs: audio_codecs, video_codecs: video_codecs} = config
445492
sdp_codecs = SDPUtils.get_rtp_codec_parameters(sdp)
446-
free_pts = get_free_payload_types(sdp_codecs)
493+
free_pts = get_free_payload_types(audio_codecs ++ video_codecs ++ sdp_codecs)
447494

448495
{audio_codecs, free_pts} = do_update_codecs(audio_codecs, sdp_codecs, free_pts)
449496
{video_codecs, _free_pts} = do_update_codecs(video_codecs, sdp_codecs, free_pts)
@@ -452,29 +499,27 @@ defmodule ExWebRTC.PeerConnection.Configuration do
452499
end
453500

454501
defp do_update_codecs(codecs, sdp_codecs, free_pts) do
455-
# we replace codec payload types in config to payload types from SDP
456-
# both normal codecs and rtx (we also update apt FMTP attribute in rtxs)
457-
# other codecs that are present in config but not in SDP
458-
# are also updated with values from a pool of free payload types (not present in SDP)
459-
# to make sure they don't conflict
460-
{sdp_rtxs, sdp_codecs} = Enum.split_with(sdp_codecs, &rtx?/1)
502+
# We replace codec payload types in config to payload types from SDP
503+
# both for normal codecs and rtx (we also update apt FMTP attribute in rtxs).
504+
# Other codecs that are present in config but not in SDP, and their
505+
# payload type is already present in SDP, are also updated with values
506+
# from a pool of free payload types (not present in SDP) to make sure they don't conflict
461507
{rtxs, codecs} = Enum.split_with(codecs, &rtx?/1)
462508

463509
{codecs, {free_pts, mapping}} =
464510
Enum.map_reduce(codecs, {free_pts, %{}}, fn codec, {free_pts, mapping} ->
465-
sdp_codecs
466-
|> Enum.find(
467-
&(String.downcase(&1.mime_type) == String.downcase(codec.mime_type) and
468-
&1.clock_rate == codec.clock_rate and
469-
&1.channels == codec.channels)
470-
)
471-
|> case do
472-
nil ->
511+
case find_in_sdp_codecs(sdp_codecs, codec) do
512+
# there is no such codec and its payload type is not used
513+
{nil, false} ->
514+
{codec, {free_pts, Map.put(mapping, codec.payload_type, codec.payload_type)}}
515+
516+
# there is no such codec, but its payload type is used
517+
{nil, true} ->
473518
[pt | rest] = free_pts
474519
new_codec = do_update_codec(codec, pt)
475520
{new_codec, {rest, Map.put(mapping, codec.payload_type, pt)}}
476521

477-
other ->
522+
{other, _pt_used} ->
478523
new_codec = do_update_codec(codec, other.payload_type)
479524
{new_codec, {free_pts, Map.put(mapping, codec.payload_type, other.payload_type)}}
480525
end
@@ -486,15 +531,18 @@ defmodule ExWebRTC.PeerConnection.Configuration do
486531
%RTPCodecParameters{rtx | sdp_fmtp_line: %FMTP{fmtp | apt: Map.fetch!(mapping, apt)}}
487532
end)
488533
|> Enum.map_reduce(free_pts, fn rtx, free_pts ->
489-
sdp_rtxs
490-
|> Enum.find(&(&1.sdp_fmtp_line.apt == rtx.sdp_fmtp_line.apt))
491-
|> case do
492-
nil ->
534+
case find_in_sdp_codecs(sdp_codecs, rtx) do
535+
# there is no such codec and its payload type is not used
536+
{nil, false} ->
537+
{rtx, free_pts}
538+
539+
# there is no such codec, but its payload type is used
540+
{nil, true} ->
493541
[pt | rest] = free_pts
494542
rtx = do_update_codec(rtx, pt)
495543
{rtx, rest}
496544

497-
other ->
545+
{other, _pt_used} ->
498546
rtx = do_update_codec(rtx, other.payload_type)
499547
{rtx, free_pts}
500548
end
@@ -503,6 +551,38 @@ defmodule ExWebRTC.PeerConnection.Configuration do
503551
{codecs ++ rtxs, free_pts}
504552
end
505553

554+
# Searches for codec in sdp_codecs.
555+
# If codec is not found, pt_used determines whether
556+
# codec's payload type is already present in sdp_codecs.
557+
# Otherwise, pt_used can have any value.
558+
defp find_in_sdp_codecs(sdp_codecs, codec, pt_used \\ false)
559+
560+
defp find_in_sdp_codecs([], _codec, pt_used), do: {nil, pt_used}
561+
562+
defp find_in_sdp_codecs([sdp_codec | sdp_codecs], codec, pt_used) do
563+
if String.ends_with?(codec.mime_type, "/rtx") do
564+
if sdp_codec.sdp_fmtp_line != nil && sdp_codec.sdp_fmtp_line.apt == codec.sdp_fmtp_line.apt do
565+
{sdp_codec, pt_used}
566+
else
567+
find_in_sdp_codecs(
568+
sdp_codecs,
569+
codec,
570+
pt_used || sdp_codec.payload_type == codec.payload_type
571+
)
572+
end
573+
else
574+
if codec_equal_soft?(sdp_codec, codec) do
575+
{sdp_codec, pt_used}
576+
else
577+
find_in_sdp_codecs(
578+
sdp_codecs,
579+
codec,
580+
pt_used || sdp_codec.payload_type == codec.payload_type
581+
)
582+
end
583+
end
584+
end
585+
506586
defp do_update_codec(codec, new_pt) do
507587
%RTPCodecParameters{rtcp_fbs: fbs, sdp_fmtp_line: fmtp} = codec
508588
new_fbs = Enum.map(fbs, &%RTCPFeedback{&1 | pt: new_pt})
@@ -515,7 +595,7 @@ defmodule ExWebRTC.PeerConnection.Configuration do
515595
def intersect_codecs(config, mline) do
516596
# we assume that this function is called after
517597
# the config was updated based on the remote SDP
518-
# so the payload types should match
598+
# so the payload types (in codec_equal?) should match
519599
codecs =
520600
case mline.type do
521601
:audio -> config.audio_codecs
@@ -526,13 +606,7 @@ defmodule ExWebRTC.PeerConnection.Configuration do
526606
|> SDPUtils.get_rtp_codec_parameters()
527607
|> Enum.flat_map(fn sdp_codec ->
528608
codecs
529-
|> Enum.find(
530-
# as of now, we ignore sdp_fmtp_line
531-
&(String.downcase(&1.mime_type) == String.downcase(sdp_codec.mime_type) and
532-
&1.payload_type == sdp_codec.payload_type and
533-
&1.clock_rate == sdp_codec.clock_rate and
534-
&1.channels == sdp_codec.channels)
535-
)
609+
|> Enum.find(&codec_equal?(&1, sdp_codec))
536610
|> case do
537611
nil ->
538612
[]
@@ -544,6 +618,36 @@ defmodule ExWebRTC.PeerConnection.Configuration do
544618
end)
545619
end
546620

621+
# soft functions does not compare payload types
622+
@doc false
623+
@spec codec_equal?(RTPCodecParameters.t(), RTPCodecParameters.t()) :: boolean()
624+
def codec_equal?(c1, c2) do
625+
String.downcase(c1.mime_type) == String.downcase(c2.mime_type) and
626+
c1.payload_type == c2.payload_type and
627+
c1.clock_rate == c2.clock_rate and
628+
c1.channels == c2.channels and fmtp_equal?(c1, c2)
629+
end
630+
631+
defp codec_equal_soft?(c1, c2) do
632+
String.downcase(c1.mime_type) == String.downcase(c2.mime_type) and
633+
c1.clock_rate == c2.clock_rate and
634+
c1.channels == c2.channels and fmtp_equal_soft?(c1, c2)
635+
end
636+
637+
defp fmtp_equal?(%{sdp_fmtp_line: nil}, _c2), do: true
638+
defp fmtp_equal?(_c1, %{sdp_fmtp_line: nil}), do: true
639+
defp fmtp_equal?(c1, c2), do: c1.sdp_fmtp_line == c2.sdp_fmtp_line
640+
641+
defp fmtp_equal_soft?(%{sdp_fmtp_line: nil}, _c2), do: true
642+
defp fmtp_equal_soft?(_c1, %{sdp_fmtp_line: nil}), do: true
643+
644+
defp fmtp_equal_soft?(c1, c2) do
645+
fmtp1 = %{c1.sdp_fmtp_line | pt: nil}
646+
fmtp2 = %{c2.sdp_fmtp_line | pt: nil}
647+
648+
fmtp1 == fmtp2
649+
end
650+
547651
@doc false
548652
@spec intersect_extensions(t(), ExSDP.Media.t()) :: [Extmap.t()]
549653
def intersect_extensions(config, mline) do

0 commit comments

Comments
 (0)