Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
mickel8 committed Nov 7, 2023
1 parent e3f734e commit 4b30dc9
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 48 deletions.
40 changes: 17 additions & 23 deletions lib/ex_webrtc/peer_connection.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 ####
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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 ->
Expand Down
166 changes: 156 additions & 10 deletions lib/ex_webrtc/peer_connection/configuration.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -12,32 +61,129 @@ 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
* rtcp_mux_policy - require
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
9 changes: 9 additions & 0 deletions lib/ex_webrtc/rtp_codec_parameters.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 25 additions & 15 deletions lib/ex_webrtc/rtp_transceiver.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}

Expand All @@ -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__{
Expand All @@ -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
2 changes: 2 additions & 0 deletions test/peer_connection_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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))

Check warning on line 115 in test/peer_connection_test.exs

View workflow job for this annotation

GitHub Actions / Lint (OTP 26 / Elixir 1.15)

There should be no calls to dbg.
:ok = PeerConnection.set_local_description(pc2, answer)

:ok = PeerConnection.set_remote_description(pc1, answer)
Expand Down

0 comments on commit 4b30dc9

Please sign in to comment.