Current section
Files
Jump to
Current section
Files
lib/sip/outgoing_call.ex
defmodule Membrane.RTC.Engine.Endpoint.SIP.OutgoingCall do
@moduledoc false
use Membrane.RTC.Engine.Endpoint.SIP.Call
require Membrane.Logger
alias Membrane.RTC.Engine.Endpoint.SIP.{Call, SippetCore}
alias Membrane.RTC.Engine.Endpoint.SIP.Call.SDP
@death_timeout_ms 5000
@response_trying 100
@response_ringing 180
@response_session_progress 183
@response_ok 200
@response_no_content 204
@response_multiple_choices 300
@response_moved_permanently 301
@response_moved_temporarily 302
@response_forbidden 403
@response_busy_here 486
@response_request_terminated 487
@response_busy_everywhere 600
@response_decline 603
## CALL MANAGEMENT API
@spec cancel(Call.id()) :: :ok
def cancel(call_id) do
GenServer.cast(Call.registry_id(call_id), :cancel)
end
@spec bye(Call.id()) :: :ok
def bye(call_id) do
GenServer.cast(Call.registry_id(call_id), :bye)
end
## SIP.Call CALLBACKS
@impl Call
def after_init(state) do
proposal = SDP.proposal(state.external_ip, state.rtp_port)
:invite
|> Call.build_headers(state, %{
content_length: byte_size(proposal),
content_type: "application/sdp"
})
|> create_request(state)
|> Map.replace(:body, proposal)
|> Call.make_request(state)
end
@impl Call
def handle_response(:invite, provisional, _response, state) when provisional in 100..199 do
case provisional do
@response_trying ->
notify_endpoint(state.endpoint, :trying)
@response_ringing ->
notify_endpoint(state.endpoint, :ringing)
@response_session_progress ->
nil
_other ->
Logger.warning("SIP Client: Unknown provisional response #{provisional}. Ignoring")
end
state
end
@impl Call
def handle_response(:invite, @response_ok, response, state) do
send_ack(response, state)
{_sn, last_request_method} = state.last_message.headers.cseq
case SDP.parse(response.body) do
# This is the case when we've already sent a CANCEL, but we received a response
# to the INVITE before the CANCEL was processed -- we still need to send the BYE
_any when last_request_method == :cancel ->
{:noreply, state} = handle_cast(:bye, state)
state
{:ok, connection_info} ->
notify_endpoint(state.endpoint, {:call_ready, connection_info})
%{state | invite_dialog_complete?: true}
{:error, reason} ->
_state = build_and_send_request(:bye, state)
# Give Sippet time to send the request (this is async)
Process.sleep(20)
raise "SIP Client: Received SDP answer is not matching our requirements: #{inspect(reason)}"
end
end
@impl Call
def handle_response(:invite, transfer, _response, state)
when transfer in [
@response_multiple_choices,
@response_moved_permanently,
@response_moved_temporarily
] do
# Callee was updated by `Call.process_response/2`.
# We need to clear the `to` header, otherwise we will receive 481 Call/Transaction Does Not Exist
after_init(%{state | to: nil})
end
@impl Call
def handle_response(:invite, declined, _response, state)
when declined in [@response_forbidden, @response_decline] do
notify_endpoint(state.endpoint, {:end, :declined})
state
end
@impl Call
def handle_response(:invite, busy, _response, state)
when busy in [@response_busy_here, @response_busy_everywhere] do
notify_endpoint(state.endpoint, {:end, :busy})
state
end
@impl Call
def handle_response(:invite, @response_request_terminated, _response, state) do
# Request Terminated -- this is the case when the INVITE was cancelled
# by a separate CANCEL request before receiving a final response
state
end
@impl Call
def handle_response(:invite, @response_no_content, _response, _state) do
# 204 is a success response, but we need SDP from the response body to setup the session
# This shouldn't ever happen, but if we don't handle it here it would get silently ignored
raise "SIP Client: Received 204 No Content in response to INVITE"
end
@impl Call
def handle_response(method, @response_ok, _response, state) when method in [:cancel, :bye] do
schedule_death(:instant)
state
end
@impl Call
def handle_response(:cancel, _any, _response, state) do
state
end
@impl Call
def handle_response(_method, status_code, response, state) do
Call.handle_generic_response(status_code, response, state)
end
@impl Call
def handle_request(:notify, request, state) do
respond(request, @response_ok)
state
end
@impl Call
def handle_request(:bye, request, state) do
respond(request, @response_ok)
notify_endpoint(state.endpoint, {:end, hangup_cause(request)})
schedule_death()
state
end
## GenServer CALLBACKS
@impl GenServer
def handle_cast(:cancel, state) do
state =
if state.invite_dialog_complete? do
build_and_send_request(:bye, state)
else
send_cancel(state)
end
notify_endpoint(state.endpoint, {:end, :cancelled})
schedule_death()
{:noreply, state}
end
@impl GenServer
def handle_cast(:bye, state) do
state = build_and_send_request(:bye, state)
notify_endpoint(state.endpoint, {:end, :user_hangup})
schedule_death()
{:noreply, state}
end
@impl GenServer
def handle_info(:die, state) do
{:stop, :normal, state}
end
## PRIVATE FUNCTIONS
defp build_and_send_request(method, state) do
method
|> Call.build_headers(state)
|> create_request(state)
|> Call.make_request(state)
end
defp create_request(%{cseq: {_cseq, method}} = headers, state) do
method
|> Sippet.Message.build_request(to_string(state.callee))
|> Map.put(:headers, headers)
end
defp respond(request, code) do
request
|> Sippet.Message.to_response(code)
|> SippetCore.send_message()
end
defp notify_endpoint(endpoint, message) do
send(endpoint, {:call_info, message})
end
defp send_ack(response, state) do
%Sippet.Message{
headers: %{
to: to,
via: [{{_major, _minor}, _protocol, {_host, _port}, %{"branch" => branch}}],
cseq: {cseq, :invite}
}
} = response
message =
:ack
|> Call.build_headers(state, %{to: to, cseq: {cseq, :ack}}, branch)
|> create_request(state)
message =
if is_nil(state.target),
do: message,
else: Map.put(message, :target, state.target)
SippetCore.send_message(message)
end
defp send_cancel(state) do
headers =
state.last_message.headers
|> Map.drop([:content_length, :content_type, :proxy_authorization, :authorization])
|> Map.update!(:cseq, fn {cseq, :invite} -> {cseq, :cancel} end)
callee = %{state.registrar_credentials.uri | userinfo: state.phone_number}
headers
|> create_request(%{state | callee: callee})
|> Call.make_request(state, false)
end
defp hangup_cause(request) do
case Sippet.Message.get_header(request, "X-Asterisk-HangupCause", []) do
["Normal Clearing"] -> :normal_clearing
[] -> :unknown
[other | _rest] -> other
end
end
defp schedule_death(), do: Process.send_after(self(), :die, @death_timeout_ms)
defp schedule_death(:instant), do: send(self(), :die)
end