Packages

Behaviour to make it easy to create custom RTMP servers

Current section

Files

Jump to
eml_gen_rtmp_server lib gen_rtmp_server protocol.ex
Raw

lib/gen_rtmp_server/protocol.ex

defmodule GenRtmpServer.Protocol do
@moduledoc """
Ranch protocol that abstracts the RTMP protocol logic away
"""
@behaviour :ranch_protocol
alias RtmpSession.Events, as: RtmpEvents
alias RtmpSession.Messages, as: RtmpMessages
use GenServer
require Logger
defmodule State do
defstruct socket: nil,
transport: nil,
session_id: nil,
bytes_read: 0,
bytes_sent: 0,
handshake_completed: false,
handshake_instance: nil,
rtmp_session_instance: nil,
gen_rtmp_server_adopter: nil,
adopter_state: nil
end
@doc "Starts the protocol for the accepted socket"
def start_link(ref, socket, transport, [module, options = %GenRtmpServer.RtmpOptions{}]) do
:proc_lib.start_link(__MODULE__, :init, [ref, socket, transport, module, options])
end
def init(ref, socket, transport, module, options) do
:ok = :proc_lib.init_ack({:ok, self()})
:ok = :ranch.accept_ack(ref)
send(self(), {:perform_handshake, module, options})
state = %State{
socket: socket,
transport: transport
}
:gen_server.enter_loop(__MODULE__, [], state)
end
def handle_info({:perform_handshake, module, initial_options}, state) do
session_id = UUID.uuid4()
{:ok, {ip, _port}} = :inet.peername(state.socket)
client_ip_string = ip |> Tuple.to_list() |> Enum.join(".")
_ = Logger.info "#{session_id}: client connected from ip #{client_ip_string}"
{handshake_instance, %RtmpHandshake.ParseResult{bytes_to_send: bytes_to_send}}
= RtmpHandshake.new(:unknown) # Let the client's handshake tell us the format
:ok = state.transport.send(state.socket, bytes_to_send)
options_list = GenRtmpServer.RtmpOptions.to_keyword_list(initial_options)
session_config = create_session_config(options_list)
new_state = %{state |
handshake_instance: handshake_instance,
session_id: session_id,
rtmp_session_instance: RtmpSession.new(0, session_id, session_config),
gen_rtmp_server_adopter: module
}
set_socket_options(new_state)
{:noreply, new_state}
end
def handle_info({:tcp, _, binary}, state = %State{handshake_completed: false}) do
case RtmpHandshake.process_bytes(state.handshake_instance, binary) do
{instance, result = %RtmpHandshake.ParseResult{current_state: :waiting_for_data}} ->
if byte_size(result.bytes_to_send) > 0, do: state.transport.send(state.socket, result.bytes_to_send)
new_state = %{state | handshake_instance: instance}
set_socket_options(new_state)
{:noreply, new_state}
{instance, result = %RtmpHandshake.ParseResult{current_state: :success}} ->
if byte_size(result.bytes_to_send) > 0, do: state.transport.send(state.socket, result.bytes_to_send)
{_, %RtmpHandshake.HandshakeResult{remaining_binary: remaining_binary}}
= RtmpHandshake.get_handshake_result(instance)
state = %{state |
handshake_instance: nil,
handshake_completed: true
}
{:ok, adopter_state} = state.gen_rtmp_server_adopter.init(state.session_id, get_ip_address(state.socket))
state = %{state | adopter_state: adopter_state}
state = process_binary(state, remaining_binary)
set_socket_options(state)
{:noreply, state}
{_, %RtmpHandshake.ParseResult{current_state: :failure}} ->
_ = Logger.info "#{state.session_id}: Client failed the handshake, disconnecting..."
state.transport.close(state.socket)
{:noreply, state}
end
end
def handle_info({:tcp, _, binary}, state = %State{}) do
state = process_binary(state, binary)
{:noreply, state}
end
def handle_info({:tcp_closed, _}, state = %State{}) do
_ = Logger.info "#{state.session_id}: socket closed"
{:stop, :normal, state}
end
def handle_info({:rtmp_send, data, send_to_stream_id, forced_timestamp}, state = %State{}) do
state = rtmp_send(data, send_to_stream_id, state, forced_timestamp)
{:noreply, state}
end
def handle_info(message, state = %State{}) do
{:ok, adopter_state} = state.gen_rtmp_server_adopter.handle_message(message, state.adopter_state)
state = %{state | adopter_state: adopter_state}
set_socket_options(state) # Just in case
{:noreply, state}
end
def code_change(old_version, state, _) do
case state.gen_rtmp_server_adopter.code_change(old_version, state.adopter_state) do
{:ok, new_adopter_state} ->
state = %{state | adopter_state: new_adopter_state}
{:ok, state}
{:error, reason} ->
{:error, reason}
end
end
defp set_socket_options(state = %State{}) do
# Ignore the result, as theoretically a tcp closed message should be arriving
# that will kill this connection
_ = state.transport.setopts(state.socket, active: :once, packet: :raw, buffer: 4096)
end
defp process_binary(state, binary) do
{session, results} = RtmpSession.process_bytes(state.rtmp_session_instance, binary)
state.transport.send(state.socket, results.bytes_to_send)
{state, session} = handle_event(results.events, state, session)
set_socket_options(state)
%{state |
rtmp_session_instance: session,
}
end
defp create_session_config(options) do
config = %RtmpSession.SessionConfig{}
config = case Keyword.fetch(options, :fms_version) do
{:ok, value} -> %{config| fms_version: value}
:error -> config
end
config = case Keyword.fetch(options, :chunk_size) do
{:ok, value} -> %{config| chunk_size: value}
:error -> config
end
config = case Keyword.fetch(options, :log_mode) do
{:ok, value} -> %{config| io_log_mode: value}
:error -> config
end
config
end
defp get_ip_address(socket) do
{:ok, {ip, _port}} = :inet.peername(socket)
ip |> Tuple.to_list() |> Enum.join(".")
end
defp handle_event([], state, session) do
{state, session}
end
defp handle_event([event = %RtmpEvents.ConnectionRequested{} | tail], state, session) do
case state.gen_rtmp_server_adopter.connection_requested(event, state.adopter_state) do
{:accepted, adopter_state} ->
_ = Logger.info("#{state.session_id}: Connection request accepted (app: '#{event.app_name}')")
state = %{state | adopter_state: adopter_state}
{session, results} = RtmpSession.accept_request(session, event.request_id)
state.transport.send(state.socket, results.bytes_to_send)
{state, session} = handle_event(results.events, state, session)
handle_event(tail, state, session)
{{:rejected, command, reason}, adopter_state} ->
_ = Logger.info("#{state.session_id}: Connection request rejected (app: '#{event.app_name}') - #{reason}")
case command do
:disconnect -> state.transport.close(state.socket)
_ -> :ok
end
state = %{state | adopter_state: adopter_state}
handle_event(tail, state, session)
end
end
defp handle_event([event = %RtmpEvents.PublishStreamRequested{} | tail], state, session) do
case state.gen_rtmp_server_adopter.publish_requested(event, state.adopter_state) do
{:accepted, adopter_state} ->
_ = Logger.info("#{state.session_id}: Publish stream request accepted (app: '#{event.app_name}', key: '#{event.stream_key}')")
handle_accepted_request(state, session, adopter_state, event.request_id, tail)
{{:rejected, command, reason}, adopter_state} ->
_ = Logger.info("#{state.session_id}: Publish stream request rejected (app: '#{event.app_name}', key: '#{event.stream_key}') - #{reason}")
handle_rejected_request(command, state, session, adopter_state, event.request_id, tail)
end
end
defp handle_event([event = %RtmpEvents.PublishingFinished{} | tail], state, session) do
{:ok, adopter_state} = state.gen_rtmp_server_adopter.publish_finished(event, state.adopter_state)
state = %{state | adopter_state: adopter_state}
handle_event(tail, state, session)
end
defp handle_event([%RtmpEvents.PeerChunkSizeChanged{} | tail], state, session) do
handle_event(tail, state, session)
end
defp handle_event([event = %RtmpEvents.AudioVideoDataReceived{} | tail], state, session) do
{:ok, adopter_state} = state.gen_rtmp_server_adopter.audio_video_data_received(event, state.adopter_state)
state = %{state | adopter_state: adopter_state}
handle_event(tail, state, session)
end
defp handle_event([event = %RtmpEvents.StreamMetaDataChanged{} | tail], state, session) do
{:ok, adopter_state} = state.gen_rtmp_server_adopter.metadata_received(event, state.adopter_state)
state = %{state | adopter_state: adopter_state}
handle_event(tail, state, session)
end
defp handle_event([event = %RtmpEvents.PlayStreamRequested{} | tail], state, session) do
case state.gen_rtmp_server_adopter.play_requested(event, state.adopter_state) do
{:accepted, adopter_state} ->
_ = Logger.info("#{state.session_id}: Play stream request accepted (app: '#{event.app_name}', key: '#{event.stream_key}')")
handle_accepted_request(state, session, adopter_state, event.request_id, tail)
{{:rejected, command, reason}, adopter_state} ->
_ = Logger.info("#{state.session_id}: Play stream request rejected (app: '#{event.app_name}', key: '#{event.stream_key}') - #{reason}")
handle_rejected_request(command, state, session, adopter_state, event.request_id, tail)
end
end
defp handle_event([event = %RtmpEvents.PlayStreamFinished{} | tail], state, session) do
{:ok, adopter_state} = state.gen_rtmp_server_adopter.play_finished(event, state.adopter_state)
state = %{state | adopter_state: adopter_state}
handle_event(tail, state, session)
end
defp handle_event([event | tail], state, session) do
_ = Logger.warn("#{state.session_id}: No code to handle RTMP session event of type #{inspect(event)}")
handle_event(tail, state, session)
end
defp handle_accepted_request(state, session, new_adopter_state, request_id, remaining_events) do
state = %{state | adopter_state: new_adopter_state}
{session, results} = RtmpSession.accept_request(session, request_id)
state.transport.send(state.socket, results.bytes_to_send)
{state, session} = handle_event(results.events, state, session)
handle_event(remaining_events, state, session)
end
defp handle_rejected_request(command, state, session, new_adopter_state, _request_id, remaining_events) do
case command do
:disconnect -> state.transport.close(state.socket)
_ -> :ok
end
state = %{state | adopter_state: new_adopter_state}
handle_event(remaining_events, state, session)
end
defp rtmp_send(data, send_to_stream_id, state, forced_timestamp) do
message = form_outbound_rtmp_message(data)
{session, results} = RtmpSession.send_rtmp_message(state.rtmp_session_instance, send_to_stream_id, message, forced_timestamp)
state.transport.send(state.socket, results.bytes_to_send)
%{state | rtmp_session_instance: session}
end
defp form_outbound_rtmp_message(av_data = %GenRtmpServer.AudioVideoData{}) do
# time_now = :os.system_time(:milli_seconds)
# time_diff = time_now - av_data.received_at_timestamp
#Logger.debug("AV Time To Resend: #{time_diff}ms (receved at #{av_data.received_at_timestamp}, now: #{time_now}")
case av_data.data_type do
:audio -> %RtmpMessages.AudioData{data: av_data.data}
:video -> %RtmpMessages.VideoData{data: av_data.data}
end
end
defp form_outbound_rtmp_message(%GenRtmpServer.MetaData{details: metadata}) do
%RtmpMessages.Amf0Data{parameters: [
"onMetaData",
%{
"width" => metadata.video_width,
"height" => metadata.video_height,
"framerate" => metadata.video_frame_rate,
"videocodecid" => metadata.video_codec,
"audiocodecid" => metadata.audio_codec,
"audiochannels" => metadata.audio_channels,
"audiodatarate" => metadata.audio_bitrate_kbps,
"audiosamplerate" => metadata.audio_sample_rate,
"videodatarate" => metadata.video_bitrate_kbps
}
]}
end
defp form_outbound_rtmp_message(data) do
raise("No known way to form outbound rtmp message for data: #{inspect(data)}")
end
end