diff --git a/lib/ex_webrtc/peer_connection.ex b/lib/ex_webrtc/peer_connection.ex index 41364146..c90ac95d 100644 --- a/lib/ex_webrtc/peer_connection.ex +++ b/lib/ex_webrtc/peer_connection.ex @@ -9,6 +9,7 @@ defmodule ExWebRTC.PeerConnection do require Logger + alias ExWebRTC.RTPCodecParameters alias __MODULE__.{Configuration, Demuxer, TWCCRecorder} alias ExWebRTC.{ @@ -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) """ @@ -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} diff --git a/lib/ex_webrtc/rtp_sender.ex b/lib/ex_webrtc/rtp_sender.ex index 61b92531..87427dad 100644 --- a/lib/ex_webrtc/rtp_sender.ex +++ b/lib/ex_webrtc/rtp_sender.ex @@ -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} @@ -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, @@ -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, @@ -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 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 @@ -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, @@ -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 diff --git a/lib/ex_webrtc/rtp_transceiver.ex b/lib/ex_webrtc/rtp_transceiver.ex index f1effb41..f825b2b7 100644 --- a/lib/ex_webrtc/rtp_transceiver.ex +++ b/lib/ex_webrtc/rtp_transceiver.ex @@ -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