Skip to content

Commit

Permalink
Add set_sender_codec
Browse files Browse the repository at this point in the history
  • Loading branch information
mickel8 committed Jan 30, 2025
1 parent 7f3b1bd commit 2af9b7f
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 3 deletions.
75 changes: 75 additions & 0 deletions lib/ex_webrtc/peer_connection.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defmodule ExWebRTC.PeerConnection do

require Logger

alias ExWebRTC.RTPCodecParameters
alias __MODULE__.{Configuration, Demuxer, TWCCRecorder}

alias ExWebRTC.{
Expand Down Expand Up @@ -152,9 +153,58 @@ defmodule ExWebRTC.PeerConnection do
GenServer.call(peer_connection, :get_configuration)
end

@doc """
Sets the codec that will be used for sending RTP packets.
`send_rtp/4` overrides some of the RTP packet fields.
In particular, when multiple codecs are negotiated, `send_rtp/4` will use
payload type of the most preffered by the remote side codec (i.e. the first
one from the list of codecs in the remote description).
Use this function if you want to select, which codec (hence payload type)
should be used for sending.
Although very unlikely, keep in mind that after renegotiation,
the selected codec may no longer be supported by the remote side and you might
need to call this function again, passing a new codec.
To check available codecs you can use `get_transceivers/1`:
```
{:ok, pc} = PeerConnection.start_link()
{:ok, rtp_sender} = PeerConnection.add_track(MediaStreamTrack.new(:video))
tr =
pc
|> PeerConnection.get_transceivers()
|> Enum.find(fn tr -> tr.sender.id == rtp_sender.id end)
dbg(tr.codecs) # list of supported codecs both for sending and receiving
# e.g. always prefer h264 over vp8
h264 = Enum.find(tr.codecs, fn codec -> codec.mime_type == "video/H264" end)
vp8 = Enum.find(tr.codecs, fn codec -> codec.mime_type == "video/VP8" end)
:ok = PeerConnection.set_sender_codec(pc, rtp_sender.id, h264 || vp8)
```
"""
@spec set_sender_codec(peer_connection(), RTPSender.id(), RTPCodecParameters.t()) ::
:ok | {:error, term()}
def set_sender_codec(peer_connection, sender_id, codec) do
GenServer.call(peer_connection, {:set_sender_codec, sender_id, codec})
end

@doc """
Sends an RTP packet to the remote peer using the track specified by the `track_id`.
The following fields of the RTP packet will be overwritten by this function:
* payload type
* ssrc
* rtp header extensions
If you negotiated multiple codecs (hence payload types) and you want to choose,
which one should be used, see `set_sender_codec/3`.
Options:
* `rtx?` - send the packet as if it was retransmitted (use SSRC and payload type specific to RTX)
"""
Expand Down Expand Up @@ -576,6 +626,31 @@ defmodule ExWebRTC.PeerConnection do
{:reply, state.config, state}
end

@impl true
def handle_call({:set_sender_codec, sender_id, codec}, _from, state) do
state.transceivers
|> Enum.with_index()
|> Enum.find(fn {tr, _idx} -> tr.sender.id == sender_id end)
|> case do
{tr, idx} when tr.direction in [:sendrecv, :sendonly] ->
case RTPTransceiver.set_sender_codec(tr, codec) do
{:ok, tr} ->
transceivers = List.replace_at(state.transceivers, idx, tr)
state = %{state | transceivers: transceivers}
{:reply, :ok, state}

{:error, _reason} = error ->
{:reply, error, state}
end

{_tr, _idx} ->
{:reply, {:error, :invalid_transceiver_direction}, state}

nil ->
{:reply, {:error, :invalid_sender_id}, state}
end
end

@impl true
def handle_call(:get_connection_state, _from, state) do
{:reply, state.conn_state, state}
Expand Down
43 changes: 40 additions & 3 deletions lib/ex_webrtc/rtp_sender.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule ExWebRTC.RTPSender do
@moduledoc """
Implementation of the [RTCRtpSender](https://www.w3.org/TR/webrtc/#rtcrtpsender-interface).
"""
require Logger

alias ExRTCP.Packet.{TransportFeedback.NACK, PayloadFeedback.PLI}
alias ExWebRTC.{MediaStreamTrack, RTPCodecParameters, Utils}
Expand All @@ -17,6 +18,7 @@ defmodule ExWebRTC.RTPSender do
id: id(),
track: MediaStreamTrack.t() | nil,
codec: RTPCodecParameters.t() | nil,
rtx_codec: RTPCodecParameters.t() | nil,
codecs: [RTPCodecParameters.t()],
rtp_hdr_exts: %{Extmap.extension_id() => Extmap.t()},
mid: String.t() | nil,
Expand Down Expand Up @@ -93,6 +95,7 @@ defmodule ExWebRTC.RTPSender do
id: Utils.generate_id(),
track: track,
codec: codec,
rtx_codec: rtx_codec,
codecs: codecs,
rtp_hdr_exts: rtp_hdr_exts,
pt: pt,
Expand All @@ -118,11 +121,32 @@ defmodule ExWebRTC.RTPSender do
@spec update(sender(), String.t(), [RTPCodecParameters.t()], [Extmap.t()]) :: sender()
def update(sender, mid, codecs, rtp_hdr_exts) do

Check warning on line 122 in lib/ex_webrtc/rtp_sender.ex

View workflow job for this annotation

GitHub Actions / CI on OTP 27 / Elixir 1.17

Function is too complex (cyclomatic complexity is 12, max is 9).
if sender.mid != nil and mid != sender.mid, do: raise(ArgumentError)

{codec, rtx_codec} = get_default_codec(codecs)

# convert to a map to be able to find extension id using extension uri
rtp_hdr_exts = Map.new(rtp_hdr_exts, fn extmap -> {extmap.uri, extmap} end)

# Keep already selected codec if it is still supported.
# Otherwise, clear it and wait until user sets it again.
codec = if sender.codec in codecs, do: sender.codec, else: nil
rtx_codec = codec && find_associated_rtx_codec(codecs, codec)

if sender.codec != nil and codec == nil do
Logger.debug("""
Unselecting RTP sender codec as it is no longer supported by the remote side.
Call set_sender_codec again passing supported codec.
Codec: #{inspect(sender.codec)}
Currently negotiated codecs: #{inspect(codecs)}
""")
end

if sender.rtx_codec != nil and rtx_codec == nil do
Logger.warning("""
Unselecting RTX RTP codec as there is no longer RTX codec for selected codec.
Retransmissions won't work starting from this moment.
Codec: #{inspect(sender.codec)}
Currently negotiated codecs: #{inspect(codecs)}
""")
end

# TODO: handle cases when codec == nil (no valid codecs after negotiation)
pt = if codec != nil, do: codec.payload_type, else: nil
rtx_pt = if rtx_codec != nil, do: rtx_codec.payload_type, else: nil
Expand All @@ -136,6 +160,7 @@ defmodule ExWebRTC.RTPSender do
sender
| mid: mid,
codec: codec,
rtx_codec: rtx_codec,
codecs: codecs,
rtp_hdr_exts: rtp_hdr_exts,
pt: pt,
Expand Down Expand Up @@ -221,6 +246,18 @@ defmodule ExWebRTC.RTPSender do
[fid | ssrc_attrs]
end

@doc false
@spec set_codec(sender(), RTPCodecParameters.t()) :: {:ok, sender()} | {:error, term()}
def set_codec(sender, codec) do
if codec in sender.codecs do
rtx_codec = find_associated_rtx_codec(sender.codecs, codec)
sender = %{sender | codec: codec, rtx_codec: rtx_codec}
{:ok, sender}
else
{:error, :invalid_codec}
end
end

@doc false
@spec send_packet(sender(), ExRTP.Packet.t(), boolean()) :: {binary(), sender()}
def send_packet(sender, packet, rtx?) do
Expand Down
10 changes: 10 additions & 0 deletions lib/ex_webrtc/rtp_transceiver.ex
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,16 @@ defmodule ExWebRTC.RTPTransceiver do
%{transceiver | direction: direction}
end

@doc false
@spec set_sender_codec(transceiver(), RTPCodecParameters.t()) ::
{:ok, transceiver()} | {:error, term()}
def set_sender_codec(transceiver, codec) do
case RTPSender.set_codec(transceiver.sender, codec) do
{:ok, sender} -> {:ok, %{transceiver | sender: sender}}
{:error, _reason} = error -> error
end
end

@doc false
@spec can_add_track?(transceiver(), kind()) :: boolean()
def can_add_track?(transceiver, kind) do
Expand Down

0 comments on commit 2af9b7f

Please sign in to comment.