Skip to content

Commit

Permalink
Add initial implementation of create_offer and add_transceiver
Browse files Browse the repository at this point in the history
  • Loading branch information
LVala committed Oct 25, 2023
1 parent fd81504 commit 3aba149
Show file tree
Hide file tree
Showing 2 changed files with 169 additions and 19 deletions.
157 changes: 151 additions & 6 deletions lib/ex_webrtc/peer_connection.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ defmodule ExWebRTC.PeerConnection do
@type offer_options() :: [ice_restart: boolean()]
@type answer_options() :: []

@type transceiver_options() :: [
direction: RTPTransceiver.direction(),
send_encodings: [:TODO],
streams: [:TODO]
]

@enforce_keys [:config, :owner]
defstruct @enforce_keys ++
[
Expand All @@ -35,7 +41,10 @@ defmodule ExWebRTC.PeerConnection do
:dtls_buffered_packets,
dtls_finished: false,
transceivers: [],
signaling_state: :stable
signaling_state: :stable,
next_mid: 0,
last_offer: nil,
last_answer: nil
]

#### API ####
Expand Down Expand Up @@ -83,6 +92,15 @@ 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})
end

#### CALLBACKS ####

@impl true
Expand All @@ -109,9 +127,68 @@ defmodule ExWebRTC.PeerConnection do
{:ok, state}
end

@impl true
def handle_call({:create_offer, options}, _from, state)
when state.signaling_state in [:stable, :have_local_offer, :have_remote_pranswer] do
# TODO: handle subsequent offers

if Keyword.get(options, :ice_restart, false) do
ICEAgent.restart(state.ice_agent)
end

# we need to asign unique mid values for the transceivers
# in this case internal counter is used

# TODO: set counter so its greater than any mid in remote description or own transcevers mids
next_mid = find_next_mid(state.next_mid)
{transceivers, next_mid} = assign_mids(state.transceivers, next_mid)

{:ok, ice_ufrag, ice_pwd} = ICEAgent.get_local_credentials(state.ice_agent)
{:ok, dtls_fingerprint} = ExDTLS.get_cert_fingerprint(state.dtls_client)

offer =
%ExSDP{ExSDP.new() | timing: %ExSDP.Timing{start_time: 0, stop_time: 0}}
|> ExSDP.add_attribute({:ice_options, "trickle"})

config =
[
ice_ufrag: ice_ufrag,
ice_pwd: ice_pwd,
ice_options: "trickle",
fingerprint: {:sha256, Utils.hex_dump(dtls_fingerprint)},
setup: :actpass,
rtcp: true
]

mlines =
Enum.map(transceivers, fn transceiver ->
RTPTransceiver.to_mline(transceiver, config)
end)

mids =
Enum.map(mlines, fn mline ->
{:mid, mid} = ExSDP.Media.get_attribute(mline, :mid)
mid
end)

offer =
offer
|> ExSDP.add_attributes([
%ExSDP.Attribute.Group{semantics: "BUNDLE", mids: mids},
"extmap-allow-mixed"
])
|> ExSDP.add_media(mlines)

sdp = to_string(offer)
desc = %SessionDescription{type: :offer, sdp: sdp}
state = %{state | next_mid: next_mid, last_offer: sdp}

{:reply, {:ok, desc}, state}
end

@impl true
def handle_call({:create_offer, _options}, _from, state) do
{:reply, :ok, state}
{:reply, {:error, :invalid_state}, state}
end

@impl true
Expand Down Expand Up @@ -155,18 +232,37 @@ defmodule ExWebRTC.PeerConnection do
])
|> ExSDP.add_media(mlines)

desc = %SessionDescription{type: :answer, sdp: to_string(answer)}
sdp = to_string(answer)
desc = %SessionDescription{type: :answer, sdp: sdp}
state = %{state | last_answer: sdp}

{:reply, {:ok, desc}, state}
end

@impl true
def handle_call({:create_answer, _options}, _from, state) do
{:reply, {:error, :invalid_state}, state}
end

@impl true
def handle_call({:set_local_description, _desc}, _from, state) do
# temporary, so the dialyzer will shut up
maybe_next_state(:stable, :local, :offer)
def handle_call({:set_local_description, desc}, _from, state) do
%SessionDescription{type: type, sdp: sdp} = desc

case type do
:rollback ->
{:reply, :ok, state}

other_type ->
with {:ok, next_state} <- maybe_next_state(state.signaling_state, :local, other_type),
:ok <- check_desc_altered(type, sdp, state),
{:ok, sdp} <- ExSDP.parse(sdp),
{:ok, state} <- apply_local_description(other_type, sdp, state) do
{:reply, :ok, %{state | signaling_state: next_state}}
else
error -> {:reply, error, state}
end
end

{:reply, :ok, state}
end

Expand All @@ -189,6 +285,7 @@ defmodule ExWebRTC.PeerConnection do
end
end

@impl true
def handle_call({:add_ice_candidate, candidate}, _from, state) do
with "candidate:" <> attr <- candidate.candidate do
ICEAgent.add_remote_candidate(state.ice_agent, attr)
Expand All @@ -197,10 +294,31 @@ defmodule ExWebRTC.PeerConnection do
{:reply, :ok, state}
end

@impl true
def handle_call(:get_transceivers, _from, state) do
{:reply, state.transceivers, state}
end

@impl true
def handle_call({:add_transceiver, :audio, options}, _from, state) do
# TODO: proper implementation, change the :audio above to track_or_kind
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}
transceivers = List.insert_at(state.transceivers, -1, transceiver)
{:reply, {:ok, transceiver}, %{state | transceivers: transceivers}}
end

@impl true
def handle_info({:ex_ice, _from, :connected}, state) do
if state.dtls_buffered_packets do
Expand Down Expand Up @@ -278,6 +396,11 @@ defmodule ExWebRTC.PeerConnection do
{:noreply, state}
end

defp apply_local_description(_type, _sdp, state) do
# TODO: implement
{:ok, state}
end

defp apply_remote_description(_type, sdp, state) do
# TODO apply steps listed in RFC 8829 5.10
with :ok <- SDPUtils.ensure_mid(sdp),
Expand Down Expand Up @@ -319,6 +442,28 @@ defmodule ExWebRTC.PeerConnection do
end)
end

defp assign_mids(transceivers, next_mid, acc \\ [])
defp assign_mids([], next_mid, acc), do: {Enum.reverse(acc), next_mid}

defp assign_mids([transceiver | rest], next_mid, acc) when is_nil(transceiver.mid) do
transceiver = %RTPTransceiver{transceiver | mid: Integer.to_string(next_mid)}
assign_mids(rest, next_mid + 1, [transceiver | acc])
end

defp assign_mids([transceiver | rest], next_mid, acc) do
assign_mids(rest, next_mid, [transceiver | acc])
end

defp find_next_mid(next_mid) do
# TODO: implement
next_mid
end

defp check_desc_altered(:offer, sdp, %{last_offer: offer}) when sdp == offer, do: :ok
defp check_desc_altered(:offer, _sdp, _state), do: {:error, :offer_altered}
defp check_desc_altered(:answer, sdp, %{last_answer: answer}) when sdp == answer, do: :ok
defp check_desc_altered(:answer, _sdp, _state), do: {:error, :answer_altered}

# Signaling state machine, RFC 8829 3.2
defp maybe_next_state(:stable, :remote, :offer), do: {:ok, :have_remote_offer}
defp maybe_next_state(:stable, :local, :offer), do: {:ok, :have_local_offer}
Expand Down
31 changes: 18 additions & 13 deletions lib/ex_webrtc/rtp_transceiver.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ defmodule ExWebRTC.RTPTransceiver do

alias ExWebRTC.{RTPCodecParameters, RTPReceiver}

@type direction() :: :sendonly | :recvonly | :sendrecv | :inactive | :stopped
@type kind() :: :audio | :video

@type t() :: %__MODULE__{
mid: String.t(),
direction: :sendonly | :recvonly | :sendrecv | :inactive | :stopped,
kind: :audio | :video,
direction: direction(),
kind: kind(),
hdr_exts: [],
codecs: [],
rtp_receiver: nil
Expand Down Expand Up @@ -69,23 +72,25 @@ defmodule ExWebRTC.RTPTransceiver do
[rtp_mapping, codec.sdp_fmtp_line, codec.rtcp_fbs]
end)

attributes =
[
transceiver.direction,
{:mid, transceiver.mid},
{:ice_ufrag, Keyword.fetch!(config, :ice_ufrag)},
{:ice_pwd, Keyword.fetch!(config, :ice_pwd)},
{:ice_options, Keyword.fetch!(config, :ice_options)},
{:fingerprint, Keyword.fetch!(config, :fingerprint)},
{:setup, Keyword.fetch!(config, :setup)},
:rtcp_mux
] ++ if(Keyword.get(config, :rtcp, false), do: [{"rtcp", "9 IN IP4 0.0.0.0"}], else: [])

%ExSDP.Media{
ExSDP.Media.new(transceiver.kind, 9, "UDP/TLS/RTP/SAVPF", pt)
| # mline must be followed by a cline, which must contain
# the default value "IN IP4 0.0.0.0" (as there are no candidates yet)
connection_data: [%ExSDP.ConnectionData{address: {0, 0, 0, 0}}]
}
|> ExSDP.Media.add_attributes([
transceiver.direction,
{:mid, transceiver.mid},
{:ice_ufrag, Keyword.fetch!(config, :ice_ufrag)},
{:ice_pwd, Keyword.fetch!(config, :ice_pwd)},
{:ice_options, Keyword.fetch!(config, :ice_options)},
{:fingerprint, Keyword.fetch!(config, :fingerprint)},
{:setup, Keyword.fetch!(config, :setup)},
:rtcp_mux
])
|> ExSDP.Media.add_attributes(media_formats)
|> ExSDP.Media.add_attributes(attributes ++ media_formats)
end

defp update(transceiver, mline) do
Expand Down

0 comments on commit 3aba149

Please sign in to comment.