Skip to content

Commit a4f6fae

Browse files
committed
Add set_sender_codec
1 parent 7f3b1bd commit a4f6fae

File tree

7 files changed

+326
-56
lines changed

7 files changed

+326
-56
lines changed

lib/ex_webrtc/peer_connection.ex

Lines changed: 80 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,61 @@ 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 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+
tr =
181+
pc
182+
|> PeerConnection.get_transceivers()
183+
|> Enum.find(fn tr -> tr.sender.id == rtp_sender.id end)
184+
185+
dbg(tr.codecs) # list of supported codecs both for sending and receiving
186+
187+
# e.g. always prefer h264 over vp8
188+
h264 = Enum.find(tr.codecs, fn codec -> codec.mime_type == "video/H264" end)
189+
vp8 = Enum.find(tr.codecs, fn codec -> codec.mime_type == "video/VP8" end)
190+
191+
:ok = PeerConnection.set_sender_codec(pc, rtp_sender.id, h264 || vp8)
192+
```
193+
"""
194+
@spec set_sender_codec(peer_connection(), RTPSender.id(), RTPCodecParameters.t()) ::
195+
:ok | {:error, term()}
196+
def set_sender_codec(peer_connection, sender_id, codec) do
197+
GenServer.call(peer_connection, {:set_sender_codec, sender_id, codec})
198+
end
199+
155200
@doc """
156201
Sends an RTP packet to the remote peer using the track specified by the `track_id`.
157202
203+
The following fields of the RTP packet will be overwritten by this function:
204+
* payload type
205+
* ssrc
206+
* rtp header extensions
207+
208+
If you negotiated multiple codecs (hence payload types) and you want to choose,
209+
which one should be used, see `set_sender_codec/3`.
210+
158211
Options:
159212
* `rtx?` - send the packet as if it was retransmitted (use SSRC and payload type specific to RTX)
160213
"""
@@ -576,6 +629,31 @@ defmodule ExWebRTC.PeerConnection do
576629
{:reply, state.config, state}
577630
end
578631

632+
@impl true
633+
def handle_call({:set_sender_codec, sender_id, codec}, _from, state) do
634+
state.transceivers
635+
|> Enum.with_index()
636+
|> Enum.find(fn {tr, _idx} -> tr.sender.id == sender_id end)
637+
|> case do
638+
{tr, idx} when tr.direction in [:sendrecv, :sendonly] ->
639+
case RTPTransceiver.set_sender_codec(tr, codec) do
640+
{:ok, tr} ->
641+
transceivers = List.replace_at(state.transceivers, idx, tr)
642+
state = %{state | transceivers: transceivers}
643+
{:reply, :ok, state}
644+
645+
{:error, _reason} = error ->
646+
{:reply, error, state}
647+
end
648+
649+
{_tr, _idx} ->
650+
{:reply, {:error, :invalid_transceiver_direction}, state}
651+
652+
nil ->
653+
{:reply, {:error, :invalid_sender_id}, state}
654+
end
655+
end
656+
579657
@impl true
580658
def handle_call(:get_connection_state, _from, state) do
581659
{:reply, state.conn_state, state}
@@ -1135,7 +1213,8 @@ defmodule ExWebRTC.PeerConnection do
11351213
end
11361214

11371215
{packet, tr} = RTPTransceiver.send_packet(tr, packet, rtx?)
1138-
:ok = DTLSTransport.send_rtp(state.dtls_transport, packet)
1216+
1217+
if packet != <<>>, do: :ok = DTLSTransport.send_rtp(state.dtls_transport, packet)
11391218

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

lib/ex_webrtc/rtp_sender.ex

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ defmodule ExWebRTC.RTPSender do
22
@moduledoc """
33
Implementation of the [RTCRtpSender](https://www.w3.org/TR/webrtc/#rtcrtpsender-interface).
44
"""
5+
require Logger
56

67
alias ExRTCP.Packet.{TransportFeedback.NACK, PayloadFeedback.PLI}
78
alias ExWebRTC.{MediaStreamTrack, RTPCodecParameters, Utils}
@@ -17,6 +18,7 @@ defmodule ExWebRTC.RTPSender do
1718
id: id(),
1819
track: MediaStreamTrack.t() | nil,
1920
codec: RTPCodecParameters.t() | nil,
21+
rtx_codec: RTPCodecParameters.t() | nil,
2022
codecs: [RTPCodecParameters.t()],
2123
rtp_hdr_exts: %{Extmap.extension_id() => Extmap.t()},
2224
mid: String.t() | nil,
@@ -93,6 +95,7 @@ defmodule ExWebRTC.RTPSender do
9395
id: Utils.generate_id(),
9496
track: track,
9597
codec: codec,
98+
rtx_codec: rtx_codec,
9699
codecs: codecs,
97100
rtp_hdr_exts: rtp_hdr_exts,
98101
pt: pt,
@@ -109,7 +112,7 @@ defmodule ExWebRTC.RTPSender do
109112
pli_count: 0,
110113
reports?: :rtcp_reports in features,
111114
outbound_rtx?: :outbound_rtx in features,
112-
report_recorder: %ReportRecorder{clock_rate: codec && codec.clock_rate},
115+
report_recorder: %ReportRecorder{},
113116
nack_responder: %NACKResponder{}
114117
}
115118
end
@@ -118,32 +121,56 @@ defmodule ExWebRTC.RTPSender do
118121
@spec update(sender(), String.t(), [RTPCodecParameters.t()], [Extmap.t()]) :: sender()
119122
def update(sender, mid, codecs, rtp_hdr_exts) do
120123
if sender.mid != nil and mid != sender.mid, do: raise(ArgumentError)
121-
122-
{codec, rtx_codec} = get_default_codec(codecs)
123-
124124
# convert to a map to be able to find extension id using extension uri
125125
rtp_hdr_exts = Map.new(rtp_hdr_exts, fn extmap -> {extmap.uri, extmap} end)
126+
127+
# Keep already selected codec if it is still supported.
128+
# Otherwise, clear it and wait until user sets it again.
129+
codec = if sender.codec in codecs, do: sender.codec, else: nil
130+
rtx_codec = codec && find_associated_rtx_codec(codecs, codec)
131+
132+
log_codec_change(sender, codec, codecs)
133+
log_rtx_codec_change(sender, rtx_codec, codecs)
134+
126135
# TODO: handle cases when codec == nil (no valid codecs after negotiation)
127136
pt = if codec != nil, do: codec.payload_type, else: nil
128137
rtx_pt = if rtx_codec != nil, do: rtx_codec.payload_type, else: nil
129138

130-
report_recorder = %ReportRecorder{
131-
sender.report_recorder
132-
| clock_rate: codec && codec.clock_rate
133-
}
134-
135139
%{
136140
sender
137141
| mid: mid,
138142
codec: codec,
143+
rtx_codec: rtx_codec,
139144
codecs: codecs,
140145
rtp_hdr_exts: rtp_hdr_exts,
141146
pt: pt,
142-
rtx_pt: rtx_pt,
143-
report_recorder: report_recorder
147+
rtx_pt: rtx_pt
144148
}
145149
end
146150

151+
defp log_codec_change(%{codec: codec} = sender, nil, neg_codecs) when codec != nil do
152+
Logger.debug("""
153+
Unselecting RTP sender codec as it is no longer supported by the remote side.
154+
Call set_sender_codec again passing supported codec.
155+
Codec: #{inspect(sender.codec)}
156+
Currently negotiated codecs: #{inspect(neg_codecs)}
157+
""")
158+
end
159+
160+
defp log_codec_change(_sender, _codec, _neg_codecs), do: :ok
161+
162+
defp log_rtx_codec_change(%{rtx_codec: rtx_codec} = sender, nil, neg_codecs)
163+
when rtx_codec != nil do
164+
Logger.debug("""
165+
Unselecting RTP sender codec as it is no longer supported by the remote side.
166+
Call set_sender_codec again passing supported codec.
167+
Codec: #{inspect(sender.codec)}
168+
Currently negotiated codecs: #{inspect(neg_codecs)}
169+
""")
170+
end
171+
172+
defp log_rtx_codec_change(_sender, _rtx_codec, _neg_codecs), do: :ok
173+
147174
@spec get_mline_attrs(sender()) :: [ExSDP.Attribute.t()]
148175
def get_mline_attrs(sender) do
149176
# Don't include track id. See RFC 8829 sec. 5.2.1
@@ -221,9 +248,53 @@ defmodule ExWebRTC.RTPSender do
221248
[fid | ssrc_attrs]
222249
end
223250

251+
@doc false
252+
@spec set_codec(sender(), RTPCodecParameters.t()) :: {:ok, sender()} | {:error, term()}
253+
def set_codec(sender, codec) do
254+
if not rtx?(codec) and supported?(sender, codec) and same_clock_rate?(sender, codec) do
255+
rtx_codec = find_associated_rtx_codec(sender.codecs, codec)
256+
sender = %{sender | codec: codec, rtx_codec: rtx_codec}
257+
{:ok, sender}
258+
else
259+
{:error, :invalid_codec}
260+
end
261+
end
262+
263+
defp rtx?(codec), do: String.ends_with?(codec.mime_type, "rtx")
264+
defp supported?(sender, codec), do: codec in sender.codecs
265+
266+
# As long as report recorder is not initialized i.e. we have not send any RTP packet
267+
# allow for codec changes. Once we start sending RTP packet, require the same clock rate.
268+
defp same_clock_rate?(%{report_recorder: %{clock_rate: nil}}, _codec), do: true
269+
defp same_clock_rate?(sender, codec), do: sender.report_recorder.clock_rate == codec.clock_rate
270+
224271
@doc false
225272
@spec send_packet(sender(), ExRTP.Packet.t(), boolean()) :: {binary(), sender()}
273+
def send_packet(%{rtx_codec: nil} = sender, _packet, true) do
274+
Logger.warning("Tried to retransmit packet but there is no selected RTX codec. Ignoring.")
275+
{<<>>, sender}
276+
end
277+
278+
def send_packet(%{codec: nil} = sender, _packet, false) do
279+
Logger.warning("Tried to send packet but there is no selected codec. Ignoring.")
280+
{<<>>, sender}
281+
end
282+
283+
def send_packet(%{packets_sent: 0}, _packet, true) do
284+
raise "Tried to retransmit packet without sending any real RTP packet. This should never happen."
285+
end
286+
287+
def send_packet(%{packets_sent: 0} = sender, packet, false) do
288+
recorder = ReportRecorder.init(sender.report_recorder, sender.codec.clock_rate, sender.ssrc)
289+
sender = %{sender | report_recorder: recorder}
290+
do_send_packet(sender, packet, false)
291+
end
292+
226293
def send_packet(sender, packet, rtx?) do
294+
do_send_packet(sender, packet, rtx?)
295+
end
296+
297+
def do_send_packet(sender, packet, rtx?) do
227298
{pt, ssrc} =
228299
if rtx? do
229300
{sender.rtx_pt, sender.rtx_ssrc}

lib/ex_webrtc/rtp_sender/report_recorder.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ defmodule ExWebRTC.RTPSender.ReportRecorder do
2929
packet_count: 0,
3030
octet_count: 0
3131

32+
@spec init(t(), non_neg_integer(), non_neg_integer()) :: t()
33+
def init(%{clock_rate: nil, sender_ssrc: nil}, clock_rate, sender_ssrc) do
34+
%__MODULE__{clock_rate: clock_rate, sender_ssrc: sender_ssrc}
35+
end
36+
37+
def init(_recorder, _clock_rate, _sender_ssrc),
38+
do: raise("Tried to re-initialize ReportRecorder")
39+
3240
@doc """
3341
Records incoming RTP packet.
3442

lib/ex_webrtc/rtp_transceiver.ex

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,16 @@ defmodule ExWebRTC.RTPTransceiver do
277277
%{transceiver | direction: direction}
278278
end
279279

280+
@doc false
281+
@spec set_sender_codec(transceiver(), RTPCodecParameters.t()) ::
282+
{:ok, transceiver()} | {:error, term()}
283+
def set_sender_codec(transceiver, codec) do
284+
case RTPSender.set_codec(transceiver.sender, codec) do
285+
{:ok, sender} -> {:ok, %{transceiver | sender: sender}}
286+
{:error, _reason} = error -> error
287+
end
288+
end
289+
280290
@doc false
281291
@spec can_add_track?(transceiver(), kind()) :: boolean()
282292
def can_add_track?(transceiver, kind) do
@@ -367,18 +377,22 @@ defmodule ExWebRTC.RTPTransceiver do
367377
@doc false
368378
@spec send_packet(transceiver(), ExRTP.Packet.t(), boolean()) :: {binary(), transceiver()}
369379
def send_packet(transceiver, packet, rtx?) do
370-
{packet, sender} = RTPSender.send_packet(transceiver.sender, packet, rtx?)
380+
case RTPSender.send_packet(transceiver.sender, packet, rtx?) do
381+
{<<>>, sender} ->
382+
{<<>>, %{transceiver | sender: sender}}
371383

372-
receiver =
373-
if rtx? do
374-
transceiver.receiver
375-
else
376-
RTPReceiver.update_sender_ssrc(transceiver.receiver, sender.ssrc)
377-
end
384+
{packet, sender} ->
385+
receiver =
386+
if rtx? do
387+
transceiver.receiver
388+
else
389+
RTPReceiver.update_sender_ssrc(transceiver.receiver, sender.ssrc)
390+
end
378391

379-
transceiver = %{transceiver | sender: sender, receiver: receiver}
392+
transceiver = %{transceiver | sender: sender, receiver: receiver}
380393

381-
{packet, transceiver}
394+
{packet, transceiver}
395+
end
382396
end
383397

384398
@doc false

test/ex_webrtc/peer_connection_test.exs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,23 @@ defmodule ExWebRTC.PeerConnectionTest do
219219
assert_receive {:ex_webrtc, _pid, {:connection_state_change, :new}}
220220
end
221221

222+
test "set_sender_codec" do
223+
{:ok, pid} = PeerConnection.start_link()
224+
{:ok, tr} = PeerConnection.add_transceiver(pid, :video)
225+
226+
{rtx_codecs, media_codecs} = Utils.split_rtx_codecs(tr.codecs)
227+
228+
assert :ok = PeerConnection.set_sender_codec(pid, tr.sender.id, List.last(media_codecs))
229+
230+
assert {:error, :invalid_sender_id} =
231+
PeerConnection.set_sender_codec(pid, "invalid_id", List.last(media_codecs))
232+
233+
:ok = PeerConnection.set_transceiver_direction(pid, tr.id, :recvonly)
234+
235+
assert {:error, :invalid_transceiver_direction} =
236+
PeerConnection.set_sender_codec(pid, tr.sender.id, List.last(media_codecs))
237+
end
238+
222239
test "send_rtp/4" do
223240
{:ok, pc1} = PeerConnection.start_link()
224241
{:ok, pc2} = PeerConnection.start_link()

test/ex_webrtc/rtp_sender/report_recorder_test.exs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ defmodule ExWebRTC.RTPSender.ReportRecorderTest do
1515
@ntp_offset 2_208_988_800
1616
@max_u32 0xFFFFFFFF
1717

18+
test "init/3" do
19+
recorder = %ReportRecorder{}
20+
21+
%{clock_rate: 90_000, sender_ssrc: 1234} =
22+
recorder = ReportRecorder.init(recorder, 90_000, 1234)
23+
24+
assert_raise RuntimeError, fn -> ReportRecorder.init(recorder, 90_000, 1234) end
25+
end
26+
1827
describe "record_packet/3" do
1928
test "keeps track of packet counts and sizes" do
2029
recorder =
@@ -78,7 +87,7 @@ defmodule ExWebRTC.RTPSender.ReportRecorderTest do
7887

7988
native_in_sec = System.convert_time_unit(1, :second, :native)
8089
seconds = 89_934
81-
# 1/8, so 0.001 in binary
90+
# 1/8, so 0.001 in binary
8291
frac = 0.125
8392

8493
assert {:ok, report, _recorder} =

0 commit comments

Comments
 (0)