The behaviour you implement to receive RTMP publishes.
One handler state exists per connection. Callbacks run inside the connection's own process, in order, and the next chunk of network data is not read until a callback returns — so a slow callback slows the publisher down instead of making memory grow (see "Backpressure" in the README).
Lifecycle
init/2 TCP connection accepted (before the RTMP handshake)
handle_connect/2 client sent `connect`
handle_publish/3 client sent `publish` — accept or reject the stream key
handle_metadata/2 client sent `onMetaData` (zero or more times)
handle_frame/2 every audio/video frame
handle_disconnect/2 the connection is gone, for whatever reasonhandle_publish/3 and handle_frame/2 are required. The others are
optional; use RTMP.Handler defines defaults (accept everything, ignore
metadata) that you can override.
There is deliberately no default for handle_publish/3: deciding which
stream keys are valid is the one thing every server must do on purpose.
Disconnect reasons
handle_disconnect/2 receives one of:
:closed— the client closed the TCP connection.:unpublished— the client sentdeleteStreamfor the published stream.:idle_timeout— no data withinidle_timeoutafter the handshake.:handshake_timeout— a handshake step took longer thanhandshake_timeout.{:rejected, reason}— yourhandle_connect/2orhandle_publish/3rejected the client, the client asked to play (not supported), or it published a codec this library rejects ({:unsupported_codec, ...}).{:disconnected, reason}— yourhandle_metadata/2orhandle_frame/2returned{:disconnect, reason, state}, orRTMP.disconnect/2was called.{:protocol_error, reason}— the client broke the protocol or a limit inRTMP.Limits.{:handler_error, exception}— one of your callbacks raised. The connection is closed cleanly and the error is logged.:shutdown— the listener is shutting down.{:socket_error, reason}— the socket failed.
handle_disconnect/2 is not called when init/2 rejected the connection,
since no state exists yet.
Example
defmodule MyApp.RTMPHandler do
use RTMP.Handler
@impl true
def handle_publish(_app, stream_key, state) do
if MyApp.Streams.valid_key?(stream_key),
do: {:ok, Map.put(state, :key, stream_key)},
else: {:reject, :invalid_key, state}
end
@impl true
def handle_frame(%RTMP.Frame{} = frame, state) do
MyApp.Pipeline.push(state.key, frame)
{:ok, state}
end
end
Summary
Types
The client's connect command, passed to handle_connect/2.
Connection details passed to init/2.
Callbacks
Called on the client's connect command.
Called once when the connection ends. The return value is ignored.
Called for every audio and video frame of the published stream, in
arrival order. Return {:disconnect, reason, state} to drop the
publisher.
Called with the stream's onMetaData, if the client sends one.
Return {:disconnect, reason, state} to drop the publisher.
Called on the client's publish command. This is where stream keys are
checked. stream_key is the publishing name exactly as sent, including
any query string ("key?token=...").
Called when a TCP connection is accepted, before the RTMP handshake.
opts is the :handler_options value given to RTMP.Listener.
Types
@type connect_info() :: %{ app: binary(), tc_url: binary() | nil, flash_ver: binary() | nil, swf_url: binary() | nil, page_url: binary() | nil, object_encoding: number(), properties: map(), remote_address: :inet.ip_address(), remote_port: :inet.port_number() }
The client's connect command, passed to handle_connect/2.
:app is the application name from the RTMP URL
(rtmp://host/<app>/<stream_key>). :properties is the complete
command object exactly as the client sent it.
@type connection_info() :: %{ remote_address: :inet.ip_address(), remote_port: :inet.port_number() }
Connection details passed to init/2.
@type reason() :: term()
@type state() :: term()
Callbacks
@callback handle_connect(connect_info(), state()) :: {:ok, state()} | {:reject, reason(), state()}
Called on the client's connect command.
Default: {:ok, state}.
Called once when the connection ends. The return value is ignored.
Default: does nothing.
@callback handle_frame(RTMP.Frame.t(), state()) :: {:ok, state()} | {:disconnect, reason(), state()}
Called for every audio and video frame of the published stream, in
arrival order. Return {:disconnect, reason, state} to drop the
publisher.
@callback handle_metadata(metadata :: map(), state()) :: {:ok, state()} | {:disconnect, reason(), state()}
Called with the stream's onMetaData, if the client sends one.
Return {:disconnect, reason, state} to drop the publisher.
Default: {:ok, state}.
@callback handle_publish(app :: binary(), stream_key :: binary(), state()) :: {:ok, state()} | {:reject, reason(), state()}
Called on the client's publish command. This is where stream keys are
checked. stream_key is the publishing name exactly as sent, including
any query string ("key?token=...").
On {:reject, reason, state} the client is told the publish failed and
the connection is closed. reason is never sent to the client.
@callback init(connection_info(), opts :: term()) :: {:ok, state()} | {:reject, reason()}
Called when a TCP connection is accepted, before the RTMP handshake.
opts is the :handler_options value given to RTMP.Listener.
Return {:reject, reason} to close the connection immediately — for
example, to refuse an IP address.
Default: {:ok, opts}.