diff --git a/lib/ex_webrtc/peer_connection.ex b/lib/ex_webrtc/peer_connection.ex index c9e1f69a..c6829b07 100644 --- a/lib/ex_webrtc/peer_connection.ex +++ b/lib/ex_webrtc/peer_connection.ex @@ -95,13 +95,10 @@ defmodule ExWebRTC.PeerConnection do GenServer.call(peer_connection, :get_transceivers) end - @spec add_transceiver( - peer_connection(), - RTPTransceiver.kind() | MediaStreamTrack.t(), - transceiver_options() - ) :: {:ok, RTPTransceiver.t()} | {:error, :TODO} - def add_transceiver(peer_connection, track_or_kind, options \\ []) do - GenServer.call(peer_connection, {:add_transceiver, track_or_kind, options}) + @spec add_transceiver(peer_connection(), RTPTransceiver.kind(), transceiver_options()) :: + {:ok, RTPTransceiver.t()} | {:error, :TODO} + def add_transceiver(peer_connection, kind, options \\ []) do + GenServer.call(peer_connection, {:add_transceiver, kind, options}) end #### CALLBACKS #### @@ -299,21 +296,17 @@ defmodule ExWebRTC.PeerConnection do end @impl true - def handle_call({:add_transceiver, :audio, options}, _from, state) do - # TODO: proper implementation, change the :audio above to track_or_kind + def handle_call({:add_transceiver, kind, options}, _from, state) + when kind in [:audio, :video] do direction = Keyword.get(options, :direction, :sendrcv) - # hardcoded audio codec - codecs = [ - %ExWebRTC.RTPCodecParameters{ - payload_type: 111, - mime_type: "audio/opus", - clock_rate: 48_000, - channels: 2 - } - ] - - transceiver = %RTPTransceiver{mid: nil, direction: direction, kind: :audio, codecs: codecs} + codecs = + case kind do + :audio -> state.config.audio_codecs + :video -> state.config.video_codecs + end + + transceiver = %RTPTransceiver{mid: nil, direction: direction, kind: kind, codecs: codecs} transceivers = List.insert_at(state.transceivers, -1, transceiver) {:reply, {:ok, transceiver}, %{state | transceivers: transceivers}} end @@ -386,7 +379,8 @@ defmodule ExWebRTC.PeerConnection do with :ok <- SDPUtils.ensure_mid(sdp), :ok <- SDPUtils.ensure_bundle(sdp), {:ok, {ice_ufrag, ice_pwd}} <- SDPUtils.get_ice_credentials(sdp), - {:ok, new_transceivers} <- update_remote_transceivers(state.transceivers, sdp) do + {:ok, new_transceivers} <- + update_remote_transceivers(state.transceivers, sdp, state.config) do :ok = ICEAgent.set_remote_credentials(state.ice_agent, ice_ufrag, ice_pwd) :ok = ICEAgent.gather_candidates(state.ice_agent) @@ -424,11 +418,11 @@ defmodule ExWebRTC.PeerConnection do end end - defp update_remote_transceivers(transceivers, sdp) do + defp update_remote_transceivers(transceivers, sdp, config) do Enum.reduce_while(sdp.media, {:ok, transceivers}, fn mline, {:ok, transceivers} -> case ExSDP.Media.get_attribute(mline, :mid) do {:mid, mid} -> - transceivers = RTPTransceiver.update_or_create(transceivers, mid, mline) + transceivers = RTPTransceiver.update_or_create(transceivers, mid, mline, config) {:cont, {:ok, transceivers}} _other -> diff --git a/lib/ex_webrtc/peer_connection/configuration.ex b/lib/ex_webrtc/peer_connection/configuration.ex index 865d8f73..671acfaa 100644 --- a/lib/ex_webrtc/peer_connection/configuration.ex +++ b/lib/ex_webrtc/peer_connection/configuration.ex @@ -3,6 +3,55 @@ defmodule ExWebRTC.PeerConnection.Configuration do PeerConnection configuration """ + alias ExWebRTC.RTPCodecParameters + + @default_audio_codecs [ + %RTPCodecParameters{ + payload_type: 111, + mime_type: "audio/opus", + clock_rate: 48_000, + channels: 2, + sdp_fmtp_line: %ExSDP.Attribute.FMTP{pt: 111, minptime: 10, useinbandfec: true}, + rtcp_fbs: [] + } + ] + + @default_video_codecs [ + %RTPCodecParameters{ + payload_type: 98, + mime_type: "video/h264", + clock_rate: 90_000, + channels: nil, + sdp_fmtp_line: %ExSDP.Attribute.FMTP{ + pt: 98, + level_asymmetry_allowed: true, + packetization_mode: 1, + profile_level_id: 0x42001F + }, + rtcp_fbs: [] + }, + %RTPCodecParameters{ + payload_type: 96, + mime_type: "video/vp8", + clock_rate: 90_000, + channels: nil, + sdp_fmtp_line: nil, + rtcp_fbs: [] + } + ] + + @rtp_hdr_extensions %{ + :mid => "urn:ietf:params:rtp-hdrext:sdes:mid", + :audio_level => "urn:ietf:params:rtp-hdrext:ssrc-audio-level" + } + + @mandatory_rtp_hdr_exts [:mid] + + @typedoc """ + Supported RTP header extensions. + """ + @type rtp_hdr_extension() :: :audio_level + @type ice_server() :: %{ optional(:credential) => String.t(), optional(:username) => String.t(), @@ -12,7 +61,18 @@ defmodule ExWebRTC.PeerConnection.Configuration do @typedoc """ Options that can be passed to `ExWebRTC.PeerConnection.start_link/1`. - Currently, ExWebRTC always uses the following config: + * `ice_servers` - list of STUN servers to use. + TURN servers are not supported right now and will be filtered out. + * `audio_codecs` - list of audio codecs to use. + Use `default_audio_codecs/0` to get a list of default audio codecs. + This option overrides default audio codecs. + If you wish to add codecs to default ones do + `audio_codecs: Configuration.default_audio_codecs() ++ my_codecs` + * `video_codecs` - the same as `audio_codecs` but for video. + * `rtp_hdr_extensions` - list of RTP header extensions to use. + MID extension is enabled by default and cannot be turned off. + + Besides options listed above, ExWebRTC uses the following config: * bundle_policy - max_bundle * ice_candidate_pool_size - 0 * ice_transport_policy - all @@ -20,24 +80,110 @@ defmodule ExWebRTC.PeerConnection.Configuration do This config cannot be changed. """ - @type options() :: [ice_servers: [ice_server()]] + @type options() :: [ + ice_servers: [ice_server()], + audio_codecs: [RTPCodecParameters.t()], + video_codecs: [RTPCodecParameters.t()], + rtp_hdr_extensions: [rtp_hdr_extension()] + ] @typedoc false - @type t() :: %__MODULE__{ice_servers: [ice_server()]} + @type t() :: %__MODULE__{ + ice_servers: [ice_server()], + audio_codecs: [RTPCodecParameters.t()], + video_codecs: [RTPCodecParameters.t()], + rtp_hdr_extensions: [rtp_hdr_extension()] + } + + defstruct ice_servers: [], + audio_codecs: @default_audio_codecs, + video_codecs: @default_video_codecs, + rtp_hdr_extensions: @mandatory_rtp_hdr_exts + + @doc """ + Returns a list of default audio codecs. + """ + @spec default_audio_codecs() :: [RTPCodecParameters.t()] + def default_audio_codecs(), do: @default_audio_codecs - defstruct ice_servers: [] + @doc """ + Returns a list of default video codecs. + """ + @spec default_video_codecs() :: [RTPCodecParameters.t()] + def default_video_codecs(), do: @default_video_codecs @doc false @spec from_options!(options()) :: t() def from_options!(options) do - config = struct!(__MODULE__, options) + options = + options + |> add_mandatory_rtp_hdr_extensions() + |> resolve_rtp_hdr_extensions() + # ATM, ExICE does not support relay via TURN + |> reject_turn_servers() + + struct!(__MODULE__, options) + end - # ATM, ExICE does not support relay via TURN - stun_servers = - config.ice_servers + @doc false + @spec is_supported_codec(t(), RTPCodecParameters.t()) :: boolean() + def is_supported_codec(config, codec) do + # This function doesn't check if rtcp-fb is supported. + # Instead, `is_supported_rtcp_fb` has to be used to filter out + # rtcp-fb that are not supported. + Enum.find( + config.audio_codecs ++ config.video_codecs, + fn supported_codec -> + # for the time of comparision, override payload type in our codec + supported_codec = + if supported_codec.sdp_fmtp_line != nil and codec.sdp_fmtp_line != nil do + %RTPCodecParameters{ + supported_codec + | sdp_fmtp_line: %ExSDP.Attribute.FMTP{ + supported_codec.sdp_fmtp_line + | pt: codec.sdp_fmtp_line.pt + } + } + else + supported_codec + end + + supported_codec.mime_type == codec.mime_type and + supported_codec.clock_rate == codec.clock_rate and + supported_codec.channels == codec.channels and + supported_codec.sdp_fmtp_line == codec.sdp_fmtp_line + end + ) + end + + @doc false + @spec is_supported_rtp_hdr_extension(t(), ExSDP.Attribute.Extmap.t()) :: boolean() + def is_supported_rtp_hdr_extension(config, rtp_hdr_extension) do + rtp_hdr_extension.uri in config.rtp_hdr_extensions + end + + @doc false + @spec is_supported_rtcp_fb(t(), ExSDP.Attribute.RTCPFeedback.t()) :: boolean() + def is_supported_rtcp_fb(_config, _rtcp_fb), do: false + + defp add_mandatory_rtp_hdr_extensions(options) do + Keyword.update(options, :rtp_hdr_extensions, @mandatory_rtp_hdr_exts, fn exts -> + exts ++ @mandatory_rtp_hdr_exts + end) + end + + defp resolve_rtp_hdr_extensions(options) do + rtp_hdr_extensions = + Enum.map(options[:rtp_hdr_extensions], fn ext -> Map.fetch!(@rtp_hdr_extensions, ext) end) + + Keyword.put(options, :rtp_hdr_extensions, rtp_hdr_extensions) + end + + defp reject_turn_servers(options) do + Keyword.update(options, :ice_servers, [], fn ice_servers -> + ice_servers |> Enum.flat_map(&List.wrap(&1.urls)) |> Enum.filter(&String.starts_with?(&1, "stun:")) - - %__MODULE__{config | ice_servers: stun_servers} + end) end end diff --git a/lib/ex_webrtc/rtp_codec_parameters.ex b/lib/ex_webrtc/rtp_codec_parameters.ex index d240f6b2..367c3068 100644 --- a/lib/ex_webrtc/rtp_codec_parameters.ex +++ b/lib/ex_webrtc/rtp_codec_parameters.ex @@ -3,6 +3,15 @@ defmodule ExWebRTC.RTPCodecParameters do RTPCodecParameters """ + @type t() :: %__MODULE__{ + payload_type: non_neg_integer(), + mime_type: binary(), + clock_rate: non_neg_integer(), + channels: non_neg_integer() | nil, + sdp_fmtp_line: ExSDP.Attribute.FMTP.t() | nil, + rtcp_fbs: [ExSDP.Attribute.RTCPFeedback.t()] + } + defstruct [:payload_type, :mime_type, :clock_rate, :channels, :sdp_fmtp_line, :rtcp_fbs] def new(type, rtp_mapping, fmtp, rtcp_fbs) do diff --git a/lib/ex_webrtc/rtp_transceiver.ex b/lib/ex_webrtc/rtp_transceiver.ex index e18d5d02..0885c1f0 100644 --- a/lib/ex_webrtc/rtp_transceiver.ex +++ b/lib/ex_webrtc/rtp_transceiver.ex @@ -3,7 +3,7 @@ defmodule ExWebRTC.RTPTransceiver do RTPTransceiver """ - alias ExWebRTC.{RTPCodecParameters, RTPReceiver} + alias ExWebRTC.{PeerConnection.Configuration, RTPCodecParameters, RTPReceiver} @type direction() :: :sendonly | :recvonly | :sendrecv | :inactive | :stopped @type kind() :: :audio | :video @@ -12,8 +12,8 @@ defmodule ExWebRTC.RTPTransceiver do mid: String.t(), direction: direction(), kind: kind(), - hdr_exts: [], - codecs: [], + hdr_exts: [ExSDP.Attribute.Extmap.t()], + codecs: [RTPCodecParameters.t()], rtp_receiver: nil } @@ -32,14 +32,14 @@ defmodule ExWebRTC.RTPTransceiver do # if it doesn't exist, creats a new one # returns list of updated transceivers @doc false - def update_or_create(transceivers, mid, mline) do + def update_or_create(transceivers, mid, mline, config) do case find_by_mid(transceivers, mid) do {idx, %__MODULE__{} = tr} -> - List.replace_at(transceivers, idx, update(tr, mline)) + List.replace_at(transceivers, idx, update(tr, mline, config)) nil -> - codecs = get_codecs(mline) - hdr_exts = ExSDP.Media.get_attributes(mline, ExSDP.Attribute.Extmap) + codecs = get_codecs(mline, config) + hdr_exts = get_rtp_hdr_extensions(mline, config) ssrc = ExSDP.Media.get_attributes(mline, ExSDP.Attribute.SSRC) tr = %__MODULE__{ @@ -55,26 +55,36 @@ defmodule ExWebRTC.RTPTransceiver do end end - defp update(transceiver, mline) do - codecs = get_codecs(mline) - hdr_exts = ExSDP.Media.get_attributes(mline, ExSDP.Attribute.Extmap) + defp update(transceiver, mline, config) do + codecs = get_codecs(mline, config) + hdr_exts = get_rtp_hdr_extensions(mline, config) ssrc = ExSDP.Media.get_attributes(mline, ExSDP.Attribute.SSRC) rtp_receiver = %RTPReceiver{ssrc: ssrc} %__MODULE__{transceiver | codecs: codecs, hdr_exts: hdr_exts, rtp_receiver: rtp_receiver} end - defp get_codecs(mline) do + defp get_codecs(mline, config) do rtp_mappings = ExSDP.Media.get_attributes(mline, ExSDP.Attribute.RTPMapping) fmtps = ExSDP.Media.get_attributes(mline, ExSDP.Attribute.FMTP) all_rtcp_fbs = ExSDP.Media.get_attributes(mline, ExSDP.Attribute.RTCPFeedback) - for rtp_mapping <- rtp_mappings do - fmtp = Enum.find(fmtps, fn fmtp -> fmtp.pt == rtp_mapping.payload_type end) + rtp_mappings + |> Stream.map(fn rtp_mapping -> + fmtp = Enum.find(fmtps, &(&1.pt == rtp_mapping.payload_type)) rtcp_fbs = - Enum.filter(all_rtcp_fbs, fn rtcp_fb -> rtcp_fb.pt == rtp_mapping.payload_type end) + all_rtcp_fbs + |> Stream.filter(&(&1.pt == rtp_mapping.payload_type)) + |> Enum.filter(&Configuration.is_supported_rtcp_fb(config, &1)) RTPCodecParameters.new(mline.type, rtp_mapping, fmtp, rtcp_fbs) - end + end) + |> Enum.filter(fn codec -> Configuration.is_supported_codec(config, codec) end) + end + + defp get_rtp_hdr_extensions(mline, config) do + mline + |> ExSDP.Media.get_attributes(ExSDP.Attribute.Extmap) + |> Enum.filter(&Configuration.is_supported_rtp_hdr_extension(config, &1)) end end diff --git a/test/peer_connection_test.exs b/test/peer_connection_test.exs index 7727767e..0ab2d5d4 100644 --- a/test/peer_connection_test.exs +++ b/test/peer_connection_test.exs @@ -102,6 +102,7 @@ defmodule ExWebRTC.PeerConnectionTest do refute_receive {:ex_webrtc, ^pc, {:track, %MediaStreamTrack{}}} end + @tag :debug test "offer/answer exchange" do {:ok, pc1} = PeerConnection.start_link() {:ok, _} = PeerConnection.add_transceiver(pc1, :audio) @@ -111,6 +112,7 @@ defmodule ExWebRTC.PeerConnectionTest do {:ok, pc2} = PeerConnection.start_link() :ok = PeerConnection.set_remote_description(pc2, offer) {:ok, answer} = PeerConnection.create_answer(pc2) + dbg(ExSDP.parse!(answer.sdp)) :ok = PeerConnection.set_local_description(pc2, answer) :ok = PeerConnection.set_remote_description(pc1, answer)