Packages
membrane_rtmp_plugin
0.16.0
0.29.5
0.29.4
0.29.3
0.29.2
0.29.1
0.29.0
0.28.1
0.28.0
0.27.3
0.27.2
0.27.0
0.26.0
0.25.0
0.24.0
0.23.3
0.23.2
0.23.1
0.23.0
0.22.1
0.22.0
0.21.0
0.20.2
0.20.1
0.20.0
0.19.3
0.19.2
retired
0.19.1
0.19.0
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.0
0.15.0
0.14.0
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.1
0.9.0
0.8.1
0.8.0
0.7.0
0.6.1
0.6.0
0.5.0
0.4.1
0.4.0
0.3.0
0.2.1
0.2.0
0.1.1
0.1.0
RTMP Plugin for Membrane Multimedia Framework
Current section
Files
Jump to
Current section
Files
lib/membrane_rtmp_plugin/rtmp/source/message_handler.ex
defmodule Membrane.RTMP.MessageHandler do
@moduledoc false
# Module responsible for processing the RTMP messages
# Appropriate responses are sent to the messages received during the initialization phase
# The data received in video and audio is forwarded to the outputs
require Membrane.Logger
alias Membrane.{Buffer, Logger}
alias Membrane.RTMP.{
Handshake,
Header,
Message,
MessageParser,
Messages,
MessageValidator,
Responses
}
alias Membrane.RTMP.Messages.Serializer
@windows_acknowledgment_size 2_500_000
@peer_bandwidth_size 2_500_000
@spec handle_client_messages(list(), map()) :: map()
def handle_client_messages([], state) do
request_packet(state.socket)
state
end
def handle_client_messages(messages, state) do
messages
|> Enum.reduce_while(state, fn {header, message}, acc ->
do_handle_client_message(message, header, acc)
end)
|> case do
{:error, :stream_validation, state} ->
state.socket_module.shutdown(state.socket, :read_write)
state
state ->
request_packet(state.socket)
%{state | actions: Enum.reverse(state.actions)}
end
end
# Expected flow of messages:
# 1. [in] c0_c1 handshake -> [out] s0_s1_s2 handshake
# 2. [in] c2 handshake -> [out] empty
# 3. [in] set chunk size -> [out] empty
# 4. [in] connect -> [out] window acknowledgement, set peer bandwidth, set chunk size, connect success, on bw done
# 5. [in] release stream -> [out] default _result
# 6. [in] FC publish, create stream, _checkbw -> [out] onFCPublish, default _result, default _result
# 7. [in] release stream -> [out] _result response
# 8. [in] publish -> [out] user control with stream id, publish success
# 9. CONNECTED
defp do_handle_client_message(%module{data: data}, header, state)
when module in [Messages.Audio, Messages.Video] do
state = get_media_actions(header, data, state)
{:cont, state}
end
defp do_handle_client_message(%Handshake.Step{type: :s0_s1_s2} = step, _header, state) do
state.socket_module.send(state.socket, Handshake.Step.serialize(step))
connection_epoch = Handshake.Step.epoch(step)
{:cont, %{state | epoch: connection_epoch}}
end
defp do_handle_client_message(%Messages.SetChunkSize{chunk_size: chunk_size}, _header, state) do
parser = %{state.message_parser | chunk_size: chunk_size}
{:cont, %{state | message_parser: parser}}
end
@validation_stage :connect
defp do_handle_client_message(%Messages.Connect{} = msg, _header, state) do
case MessageValidator.validate_connect(state.validator, msg) do
{:ok, _msg} = result ->
chunk_size = state.message_parser.chunk_size
[
%Messages.WindowAcknowledgement{size: @windows_acknowledgment_size},
%Messages.SetPeerBandwidth{size: @peer_bandwidth_size},
# stream begin type
%Messages.UserControl{event_type: 0x00, data: <<0, 0, 0, 0>>},
# by default the ffmpeg server uses 128 chunk size
%Messages.SetChunkSize{chunk_size: chunk_size}
]
|> Enum.each(&send_rtmp_payload(&1, state.socket, chunk_size))
{[tx_id], message_parser} = MessageParser.generate_tx_ids(state.message_parser, 1)
tx_id
|> Responses.connection_success()
|> send_rtmp_payload(state.socket, chunk_size, chunk_stream_id: 3)
Responses.on_bw_done()
|> send_rtmp_payload(state.socket, chunk_size, chunk_stream_id: 3)
{:cont,
validation_action(%{state | message_parser: message_parser}, @validation_stage, result)}
{:error, _reason} = error ->
{:halt, {:error, :stream_validation, validation_action(state, @validation_stage, error)}}
end
end
# According to ffmpeg's documentation, this command should make the server release channel for a media stream
# We are simply acknowleding the message
@validation_stage :release_stream
defp do_handle_client_message(
%Messages.ReleaseStream{tx_id: tx_id} = msg,
_header,
state
) do
case MessageValidator.validate_release_stream(state.validator, msg) do
{:ok, _msg} = result ->
tx_id
|> Responses.default_result()
|> send_rtmp_payload(state.socket, state.message_parser.chunk_size, chunk_stream_id: 3)
{:cont, validation_action(state, @validation_stage, result)}
{:error, _reason} = error ->
{:halt, {:error, :stream_validation, validation_action(state, @validation_stage, error)}}
end
end
@validation_stage :publish
defp do_handle_client_message(
%Messages.Publish{stream_key: stream_key} = msg,
_header,
state
) do
case MessageValidator.validate_publish(state.validator, msg) do
{:ok, _msg} = result ->
%Messages.UserControl{event_type: 0, data: <<0, 0, 0, 1>>}
|> send_rtmp_payload(state.socket, state.message_parser.chunk_size, chunk_stream_id: 3)
Responses.publish_success(stream_key)
|> send_rtmp_payload(state.socket, state.message_parser.chunk_size, chunk_stream_id: 3)
{:cont, validation_action(state, @validation_stage, result)}
{:error, _reason} = error ->
{:halt, {:error, :stream_validation, validation_action(state, @validation_stage, error)}}
end
end
# A message containing stream metadata
@validation_stage :set_data_frame
defp do_handle_client_message(%Messages.SetDataFrame{} = msg, _header, state) do
case MessageValidator.validate_set_data_frame(state.validator, msg) do
{:ok, _msg} = result ->
{:cont, validation_action(state, @validation_stage, result)}
{:error, _reason} = error ->
{:halt, {:error, :stream_validation, validation_action(state, @validation_stage, error)}}
end
end
# According to ffmpeg's documentation, this command should prepare the server to receive media streams
# We are simply acknowleding the message
defp do_handle_client_message(%Messages.FCPublish{}, _header, state) do
%Messages.Anonymous{name: "onFCPublish", properties: []}
|> send_rtmp_payload(state.socket, state.message_parser.chunk_size, chunk_stream_id: 3)
{:cont, state}
end
defp do_handle_client_message(%Messages.CreateStream{tx_id: tx_id}, _header, state) do
stream_id = [1.0]
tx_id
|> Responses.default_result(stream_id)
|> send_rtmp_payload(state.socket, state.message_parser.chunk_size, chunk_stream_id: 3)
{:cont, state}
end
# Check bandwidth message
defp do_handle_client_message(
%Messages.Anonymous{name: "_checkbw", tx_id: tx_id},
_header,
state
) do
tx_id
|> send_rtmp_payload(state.socket, state.message_parser.chunk_size, chunk_stream_id: 3)
{:cont, state}
end
defp do_handle_client_message(%Messages.Anonymous{name: "deleteStream"}, _header, state) do
{:cont, %{state | actions: [{:end_of_stream, :output} | state.actions]}}
end
defp do_handle_client_message(%Messages.Anonymous{} = message, _header, state) do
Logger.debug("Unknown message: #{inspect(message)}")
{:cont, state}
end
defp request_packet({:sslsocket, _1, _2} = socket) do
:ssl.setopts(socket, active: :once)
end
defp request_packet(socket) do
:inet.setopts(socket, active: :once)
end
defp get_media_actions(rtmp_header, data, state) do
payload =
get_flv_tag(rtmp_header, data)
|> (&if(state.header_sent?, do: &1, else: get_flv_header() <> &1)).()
actions = [{:buffer, {:output, %Buffer{payload: payload}}} | state.actions]
%{state | header_sent?: true, actions: actions}
end
defp get_flv_header() do
alias Membrane.FLV
{header, 0} =
FLV.Serializer.serialize(%FLV.Header{audio_present?: true, video_present?: true}, 0)
# Add PreviousTagSize, which is 0 for the first tag
header <> <<0::32>>
end
defp get_flv_tag(
%Membrane.RTMP.Header{
timestamp: timestamp,
body_size: data_size,
type_id: type_id,
stream_id: stream_id
},
payload
) do
tag_size = data_size + 11
<<upper_timestamp::8, lower_timestamp::24>> = <<timestamp::32>>
<<type_id::8, data_size::24, lower_timestamp::24, upper_timestamp::8, stream_id::24,
payload::binary-size(data_size), tag_size::32>>
end
defp send_rtmp_payload(message, socket, chunk_size, opts \\ []) do
type = Serializer.type(message)
body = Serializer.serialize(message)
chunk_stream_id = Keyword.get(opts, :chunk_stream_id, 2)
header =
[chunk_stream_id: chunk_stream_id, type_id: type, body_size: byte_size(body)]
|> Keyword.merge(opts)
|> Header.new()
|> Header.serialize()
payload = Message.chunk_payload(body, chunk_stream_id, chunk_size)
socket_module(socket).send(socket, [header | payload])
end
defp validation_action(state, stage, result) do
notification =
case result do
{:ok, msg} -> {:notify_parent, {:stream_validation_success, stage, msg}}
{:error, reason} -> {:notify_parent, {:stream_validation_error, stage, reason}}
end
Map.update!(state, :actions, &[notification | &1])
end
# The RTMP connection is based on TCP therefore we are operating on a continuous stream of bytes.
# In such case packets received on TCP sockets may contain a partial RTMP packet or several full packets.
#
# `MessageParser` is already able to request more data if packet is incomplete but it is not aware
# if its current buffer contains more than one message, therefore we need to call the `&MessageParser.handle_packet/2`
# as long as we decide to receive more messages (before starting to relay media packets).
#
# Once we hit `:need_more_data` the function returns the list of parsed messages and the message_parser then is ready
# to receive more data to continue with emitting new messages.
@spec parse_packet_messages(packet :: binary(), message_parser :: struct(), [{any(), any()}]) ::
{[Message.t()], message_parser :: struct()}
def parse_packet_messages(packet, message_parser, messages \\ [])
def parse_packet_messages(<<>>, %{buffer: <<>>} = message_parser, messages) do
{Enum.reverse(messages), message_parser}
end
def parse_packet_messages(packet, message_parser, messages) do
case MessageParser.handle_packet(packet, message_parser) do
{header, message, message_parser} ->
parse_packet_messages(<<>>, message_parser, [{header, message} | messages])
{:need_more_data, message_parser} ->
{Enum.reverse(messages), message_parser}
{:handshake_done, message_parser} ->
parse_packet_messages(<<>>, message_parser, messages)
{%Handshake.Step{} = step, message_parser} ->
parse_packet_messages(<<>>, message_parser, [{nil, step} | messages])
end
end
@compile {:inline, socket_module: 1}
defp socket_module({:sslsocket, _1, _2}), do: :ssl
defp socket_module(_other), do: :gen_tcp
end