Current section
Files
Jump to
Current section
Files
lib/gen_rtmp_client.ex
defmodule GenRtmpClient do
@moduledoc """
A behaviour for creating RTMP clients.
A `GenRtmpClient` abstracts out the functionality and RTMP message flow
so that modules that implement this behaviour can focus on the high level
business logic of how their RTMP client should behave.
"""
require Logger
use GenServer
alias Rtmp.ClientSession.Events, as: SessionEvents
@type adopter_module :: module
@type adopter_state :: any
@type adopter_args :: any
@type adopter_response :: {:ok, adopter_state}
@type rtmp_client_pid :: pid
@type disconnection_reason :: :stopping | :closed | :inet.posix
@callback init(GenRtmpClient.ConnectionInfo.t, adopter_args) :: {:ok, adopter_state}
@callback handle_connection_response(SessionEvents.ConnectionResponseReceived.t, adopter_state) :: adopter_response
@callback handle_play_response(SessionEvents.PlayResponseReceived.t, adopter_state) :: adopter_response
@callback handle_play_reset(SessionEvents.PlayResetReceived.t, adopter_state) :: adopter_response
@callback handle_publish_response(SessionEvents.PublishResponseReceived.t, adopter_state) :: adopter_response
@callback handle_metadata_received(SessionEvents.StreamMetaDataReceived.t, adopter_state) :: adopter_response
@callback handle_av_data_received(SessionEvents.AudioVideoDataReceived.t, adopter_state) :: adopter_response
@callback handle_disconnection(disconnection_reason, adopter_state) :: {:stop, adopter_state} | {:reconnect, adopter_state}
@callback byte_io_totals_updated(SessionEvents.NewByteIOTotals.t, adopter_state) :: adopter_response
@callback handle_message(any, adopter_state) :: adopter_response
defmodule State do
@moduledoc false
defstruct connection_status: :disconnected,
adopter_module: nil,
adopter_state: nil,
connection_info: nil,
socket: nil,
handshake_state: nil,
protocol_handler_pid: nil,
session_handler_pid: nil,
is_being_stopped: false
end
@spec start_link(adopter_module, GenRtmpClient.ConnectionInfo.t, adopter_args) :: GenServer.on_start
@doc """
Starts a new RTMP connection to the specified server. The client's logic is managed by the module
specified by the adopter_module, which is expected to adopt the `GenRtmpClient` behaviour.
"""
def start_link(adopter_module, connection_info = %GenRtmpClient.ConnectionInfo{}, adopter_args) do
GenServer.start_link(__MODULE__, [adopter_module, connection_info, adopter_args])
end
@spec stop_client(rtmp_client_pid) :: :ok
@doc "Tells an RTMP client to disconnect without retrying and permanently stop"
def stop_client(pid) do
GenServer.cast(pid, :stop_client)
end
@spec start_playback(rtmp_client_pid, Rtmp.stream_key) :: :ok
@doc "Requests playback capabilities on the specified stream key"
def start_playback(rtmp_client_pid, stream_key) do
GenServer.cast(rtmp_client_pid, {:start_playback, stream_key})
end
@spec stop_playback(rtmp_client_pid, Rtmp.stream_key) :: :ok
@doc "Stops playback on the specified stream key"
def stop_playback(rtmp_client_pid, stream_key) do
GenServer.cast(rtmp_client_pid, {:stop_playback, stream_key})
end
@spec start_publish(rtmp_client_pid, Rtmp.stream_key, Rtmp.ClientSession.Handler.publish_type) :: :ok
@doc "Requests publish capabilities for the specified stream key"
def start_publish(rtmp_client_pid, stream_key, type) do
GenServer.cast(rtmp_client_pid, {:start_publish, stream_key, type})
end
@spec stop_publish(rtmp_client_pid, Rtmp.stream_key) :: :ok
@doc "Stops publishing on the specified stream key"
def stop_publish(rtmp_client_pid, stream_key) do
GenServer.cast(rtmp_client_pid, {:stop_publish, stream_key})
end
@spec publish_metadata(rtmp_client_pid, Rtmp.stream_key, Rtmp.StreamMetadata.t) :: :ok
@doc "Sends new metadata information to the server for the specified stream key"
def publish_metadata(rtmp_client_pid, stream_key, metadata) do
GenServer.cast(rtmp_client_pid, {:publish_metadata, stream_key, metadata})
end
@spec publish_av_data(rtmp_client_pid, Rtmp.stream_key, Rtmp.ClientSession.Handler.av_type, Rtmp.timestamp, binary) :: :ok
@doc "Sends audio or video data to the server for the specified stream key"
def publish_av_data(rtmp_client_pid, stream_key, type, timestamp, data) do
GenServer.cast(rtmp_client_pid, {:publish_av_data, stream_key, type, timestamp, data})
end
@doc false
def send_event(pid, event) do
GenServer.cast(pid, {:session_event, event})
end
@doc false
def send_data(pid, binary) do
GenServer.cast(pid, {:rtmp_output, binary})
end
def init([adopter_module, connection_info, adopter_args]) do
{:ok, adopter_state} = adopter_module.init(connection_info, adopter_args)
state = %State{
adopter_module: adopter_module,
adopter_state: adopter_state,
connection_info: connection_info,
}
case connect_to_server(state) do
{:permanently_disconnected, _state} -> {:stop, :permanently_disconnected}
{:ok, state} -> {:ok, state}
end
end
def handle_cast({:rtmp_output, binary}, state) do
:gen_tcp.send(state.socket, binary)
{:noreply, state}
end
def handle_cast({:start_playback, stream_key}, state) do
:ok = Rtmp.ClientSession.Handler.request_playback(state.session_handler_pid, stream_key)
{:noreply, state}
end
def handle_cast({:start_publish, stream_key, type}, state) do
:ok = Rtmp.ClientSession.Handler.request_publish(state.session_handler_pid, stream_key, type)
{:noreply, state}
end
def handle_cast({:publish_metadata, stream_key, metadata}, state) do
:ok = Rtmp.ClientSession.Handler.publish_metadata(state.session_handler_pid, stream_key, metadata)
{:noreply, state}
end
def handle_cast({:publish_av_data, stream_key, type, timestamp, data}, state) do
:ok = Rtmp.ClientSession.Handler.publish_av_data(state.session_handler_pid, stream_key, type, timestamp, data)
{:noreply, state}
end
def handle_cast({:session_event, event}, state) do
state = handle_event(event, state)
{:noreply, state}
end
def handle_cast(:stop_client, state) do
state = %{state | is_being_stopped: true}
:gen_tcp.shutdown(state.socket, :read_write)
{:noreply, state}
end
def handle_info({:tcp, _, binary}, state = %State{connection_status: :handshaking}) do
:inet.setopts(state.socket, get_socket_options())
case Rtmp.Handshake.process_bytes(state.handshake_state, binary) do
{handshake_state, result = %Rtmp.Handshake.ParseResult{current_state: :waiting_for_data}} ->
if byte_size(result.bytes_to_send) > 0, do: :gen_tcp.send(state.socket, result.bytes_to_send)
new_state = %{state | handshake_state: handshake_state}
:inet.setopts(state.socket, get_socket_options())
{:noreply, new_state}
{handshake_state, result = %Rtmp.Handshake.ParseResult{current_state: :success}} ->
if byte_size(result.bytes_to_send) > 0, do: :gen_tcp.send(state.socket, result.bytes_to_send)
{_, %Rtmp.Handshake.HandshakeResult{remaining_binary: remaining_binary}}
= Rtmp.Handshake.get_handshake_result(handshake_state)
{:ok, protocol_pid} = Rtmp.Protocol.Handler.start_link(state.connection_info.connection_id, self(), __MODULE__)
{:ok, session_pid} = Rtmp.ClientSession.Handler.start_link(state.connection_info.connection_id, %Rtmp.ClientSession.Configuration{})
:ok = Rtmp.Protocol.Handler.set_session(protocol_pid, session_pid, Rtmp.ClientSession.Handler)
:ok = Rtmp.ClientSession.Handler.set_protocol_handler(session_pid, protocol_pid, Rtmp.Protocol.Handler)
:ok = Rtmp.ClientSession.Handler.set_event_handler(session_pid, self(), __MODULE__)
:ok = Rtmp.Protocol.Handler.notify_input(protocol_pid, remaining_binary)
state = %{state |
handshake_state: nil,
connection_status: :open,
protocol_handler_pid: protocol_pid,
session_handler_pid: session_pid,
}
_ = Logger.info "#{state.connection_info.connection_id}: handshake complete"
:ok = send_connect_command(state)
{:noreply, state}
{_, %Rtmp.Handshake.ParseResult{current_state: :failure}} ->
_ = Logger.info "#{state.connection_info.connection_id}: Client failed the handshake, disconnecting..."
:gen_tcp.close(state.socket)
{:noreply, state}
end
end
def handle_info({:tcp, _, binary}, state) do
:ok = Rtmp.Protocol.Handler.notify_input(state.protocol_handler_pid, binary)
:inet.setopts(state.socket, get_socket_options())
{:noreply, state}
end
def handle_info({:tcp_closed, _}, state) do
reason = if state.is_being_stopped, do: :stopping, else: :closed
case notify_disconnection(state, reason) do
{:permanently_disconnected, state} ->
{:stop, :normal, state}
{:ok, state} ->
# reconnected
{:noreply, state}
end
end
def handle_info(message, state) do
{:ok, adopter_state} = state.adopter_module.handle_message(message, state.adopter_state)
state = %{state | adopter_state: adopter_state}
{:noreply, state}
end
defp send_connect_command(state) do
Rtmp.ClientSession.Handler.request_connection(state.session_handler_pid, state.connection_info.app_name)
end
defp connect_to_server(state) do
case :gen_tcp.connect(String.to_charlist(state.connection_info.host), state.connection_info.port, get_socket_options()) do
{:error, reason} -> notify_disconnection(state, reason)
{:ok, socket} ->
{handshake_state, %Rtmp.Handshake.ParseResult{bytes_to_send: bytes_to_send}} = Rtmp.Handshake.new(:digest)
:ok = :gen_tcp.send(socket, bytes_to_send)
state = %{state |
socket: socket,
handshake_state: handshake_state,
connection_status: :handshaking
}
{:ok, state}
end
end
defp notify_disconnection(state, reason) do
state = %{state | connection_status: :disconnected}
case state.adopter_module.handle_disconnection(reason, state.adopter_state) do
{:stop, adopter_state} ->
{:permanently_disconnected, %{state | adopter_state: adopter_state}}
{:retry, adopter_state} ->
connect_to_server(%{state | adopter_state: adopter_state})
end
end
defp handle_event(event = %SessionEvents.AudioVideoDataReceived{}, state) do
{:ok, adopter_state} = state.adopter_module.handle_av_data_received(event, state.adopter_state)
%{state | adopter_state: adopter_state}
end
defp handle_event(event = %SessionEvents.NewByteIOTotals{}, state) do
{:ok, adopter_state} = state.adopter_module.byte_io_totals_updated(event, state.adopter_state)
%{state | adopter_state: adopter_state}
end
defp handle_event(event = %SessionEvents.ConnectionResponseReceived{}, state) do
{:ok, adopter_state} = state.adopter_module.handle_connection_response(event, state.adopter_state)
%{state | adopter_state: adopter_state}
end
defp handle_event(event = %SessionEvents.PlayResetReceived{}, state) do
{:ok, adopter_state} = state.adopter_module.handle_play_reset(event, state.adopter_state)
%{state | adopter_state: adopter_state}
end
defp handle_event(event = %SessionEvents.PlayResponseReceived{}, state) do
{:ok, adopter_state} = state.adopter_module.handle_play_response(event, state.adopter_state)
%{state | adopter_state: adopter_state}
end
defp handle_event(event = %SessionEvents.PublishResponseReceived{}, state) do
{:ok, adopter_state} = state.adopter_module.handle_publish_response(event, state.adopter_state)
%{state | adopter_state: adopter_state}
end
defp get_socket_options(), do: [:binary | Keyword.new(active: :once, packet: :raw, buffer: 4096)]
end