Parrot.MediaHandler behaviour (Parrot Platform v0.0.1-alpha.2)

Behaviour for implementing media session handlers in Parrot.

The Parrot.MediaHandler behaviour provides callbacks for handling media-specific events during SIP calls, including SDP negotiation, codec selection, media stream lifecycle, and real-time media events.

Overview

MediaHandler complements Parrot.UasHandler by providing fine-grained control over media sessions. While UasHandler manages SIP protocol events, MediaHandler focuses on the actual media streams (audio/video).

Media handlers allow applications to:

  • Control audio playback with event-driven callbacks
  • Customize SDP offer/answer negotiation
  • Influence codec selection based on your preferences
  • React to media stream lifecycle events
  • Handle errors and recover gracefully
  • Build IVR systems, voicemail, music on hold, and more

Basic Usage

defmodule MyApp.MediaHandler do
  @behaviour Parrot.MediaHandler

  @impl true
  def init(_args) do
    {:ok, %{preferred_codec: :opus}}
  end

  @impl true
  def handle_codec_negotiation(offered, supported, state) do
    # Prefer Opus over G.711
    cond do
      :opus in offered and :opus in supported ->
        {:ok, :opus, state}
      :pcmu in offered and :pcmu in supported ->
        {:ok, :pcmu, state}
      true ->
        {:error, :no_common_codec, state}
    end
  end

  @impl true
  def handle_stream_start(_session_id, :outbound, state) do
    # Play welcome message when call connects
    {{:play, "/audio/welcome.wav"}, state}
  end
  
  @impl true
  def handle_play_complete(file_path, state) do
    # After welcome, play menu or stop
    if file_path == "/audio/welcome.wav" do
      {{:play, "/audio/menu.wav"}, state}
    else
      {:stop, state}
    end
  end
end

Integration with UasHandler

Typically, you'll implement both behaviours in your application:

defmodule MyApp do
  use Parrot.UasHandler
  @behaviour Parrot.MediaHandler
  
  # Handle incoming call
  @impl true
  def handle_invite(request, state) do
    # Create media session with this module as the handler
    {:ok, _pid} = Parrot.Media.MediaSession.start_link(
      id: "call_123",
      role: :uas,
      media_handler: __MODULE__,
      handler_args: %{welcome_file: "welcome.wav"}
    )
    
    # Process SDP and respond
    case Parrot.Media.MediaSession.process_offer("call_123", request.body) do
      {:ok, sdp_answer} ->
        {:respond, 200, "OK", %{}, sdp_answer}
      {:error, _} ->
        {:respond, 488, "Not Acceptable Here", %{}, ""}
    end
  end
  
  # MediaHandler callbacks...
end

Callback Flow

The typical callback sequence for a call:

  1. init/1 - Handler initialization
  2. handle_session_start/3 - Media session created
  3. handle_offer/3 - SDP offer received (optional)
  4. handle_codec_negotiation/3 - Select codec
  5. handle_negotiation_complete/4 - Negotiation done
  6. handle_stream_start/3 - Media streaming begins
  7. handle_play_complete/2 - Audio playback events (if playing)
  8. handle_stream_stop/3 - Media streaming ends
  9. handle_session_stop/3 - Cleanup

Current Implementation

The current implementation provides:

  • G.711 (PCMU/PCMA) codec support
  • Basic media control (play, stop, pause, resume)
  • Audio file playback with completion callbacks

Summary

Types

Codec atom

SDP direction

Media actions that can be returned from callbacks.

Media session ID

Handler state - can be any term

Types

codec()

@type codec() :: :pcmu | :pcma | :opus | atom()

Codec atom

direction()

@type direction() :: :inbound | :outbound

SDP direction

media_action()

@type media_action() ::
  {:play, file_path :: String.t()}
  | {:play, file_path :: String.t(), opts :: keyword()}
  | :stop
  | :pause
  | :resume
  | {:set_codec, codec()}
  | :noreply

Media actions that can be returned from callbacks.

  • {:play, file_path} - Play an audio file
  • {:play, file_path, opts} - Play with options
  • :stop - Stop current media
  • :pause - Pause playback
  • :resume - Resume playback
  • {:set_codec, codec} - Switch codec
  • :noreply - No action

session_id()

@type session_id() :: String.t()

Media session ID

state()

@type state() :: term()

Handler state - can be any term

Callbacks

handle_answer(sdp, direction, state)

(optional)
@callback handle_answer(sdp :: String.t(), direction(), state()) ::
  {:ok, modified_sdp :: String.t(), state()}
  | {:reject, reason :: term(), state()}
  | {:noreply, state()}

Process an SDP answer.

Called before the media session finalizes an SDP answer.

Parameters

  • sdp - The SDP answer as a string
  • direction - :inbound or :outbound
  • state - Current handler state

Returns

  • {:ok, modified_sdp, state} - Use modified SDP
  • {:reject, reason, state} - Reject the answer
  • {:noreply, state} - Process SDP without modification

handle_codec_negotiation(offered_codecs, supported_codecs, state)

(optional)
@callback handle_codec_negotiation(
  offered_codecs :: [codec()],
  supported_codecs :: [codec()],
  state()
) ::
  {:ok, codec(), state()}
  | {:ok, [codec()], state()}
  | {:error, :no_common_codec, state()}

Customize codec selection.

Called during SDP negotiation to select the best codec from offered and supported lists.

Parameters

  • offered_codecs - Codecs offered by remote party
  • supported_codecs - Codecs supported locally
  • state - Current handler state

Returns

  • {:ok, codec, state} - Select a single codec
  • {:ok, codec_list, state} - Return ordered preference list
  • {:error, :no_common_codec, state} - No acceptable codec

Example

@impl true
def handle_codec_negotiation(offered, supported, state) do
  # Prefer Opus > G.711μ > G.711A
  cond do
    :opus in offered and :opus in supported ->
      {:ok, :opus, state}
    :pcmu in offered and :pcmu in supported ->
      {:ok, :pcmu, state}
    :pcma in offered and :pcma in supported ->
      {:ok, :pcma, state}
    true ->
      {:error, :no_common_codec, state}
  end
end

handle_media_request(request, state)

(optional)
@callback handle_media_request(request :: term(), state()) ::
  media_action()
  | {media_action(), state()}
  | {:error, reason :: term(), state()}

Handle custom media requests.

Allows for extensibility with custom requests.

Parameters

  • request - Custom request
  • state - Current handler state

Returns

  • media_action - Action to execute
  • {media_action, state} - Action with state update
  • {:error, reason, state} - Invalid request

handle_negotiation_complete(local_sdp, remote_sdp, selected_codec, state)

(optional)
@callback handle_negotiation_complete(
  local_sdp :: String.t(),
  remote_sdp :: String.t(),
  selected_codec :: codec(),
  state()
) :: {:ok, state()} | {:error, reason :: term(), state()}

Called after SDP negotiation completes.

Provides the final negotiated parameters.

Parameters

  • local_sdp - Final local SDP
  • remote_sdp - Final remote SDP
  • selected_codec - The negotiated codec
  • state - Current handler state

Returns

  • {:ok, state} - Negotiation accepted
  • {:error, reason, state} - Reject the negotiation

handle_offer(sdp, direction, state)

(optional)
@callback handle_offer(sdp :: String.t(), direction(), state()) ::
  {:ok, modified_sdp :: String.t(), state()}
  | {:reject, reason :: term(), state()}
  | {:noreply, state()}

Process an SDP offer.

Called before the media session processes an SDP offer. The handler can modify the SDP or reject it.

Parameters

  • sdp - The SDP offer as a string
  • direction - :inbound or :outbound
  • state - Current handler state

Returns

  • {:ok, modified_sdp, state} - Use modified SDP
  • {:reject, reason, state} - Reject the offer
  • {:noreply, state} - Process SDP without modification

handle_play_complete(file_path, state)

(optional)
@callback handle_play_complete(file_path :: String.t(), state()) ::
  media_action() | {media_action(), state()} | {:noreply, state()}

Handle playback completion.

Called when an audio file finishes playing.

Parameters

  • file_path - Path of completed file
  • state - Current handler state

Returns

  • media_action - Next action to execute
  • {media_action, state} - Action with state update
  • {:noreply, state} - No action

handle_session_start(session_id, opts, state)

(optional)
@callback handle_session_start(session_id(), opts :: keyword(), state()) ::
  {:ok, state()} | {:error, reason :: term(), state()}

Handle media session start.

Called when a media session is being established.

Parameters

  • session_id - Unique session identifier
  • opts - Session options
  • state - Current handler state

Returns

  • {:ok, state} - Session started successfully
  • {:error, reason, state} - Session start failed

handle_session_stop(session_id, reason, state)

(optional)
@callback handle_session_stop(session_id(), reason :: term(), state()) :: {:ok, state()}

Handle media session stop.

Called when a media session is terminating.

Parameters

  • session_id - Session identifier
  • reason - Termination reason
  • state - Current handler state

Returns

  • {:ok, state} - Acknowledged

handle_stream_error(session_id, error, state)

(optional)
@callback handle_stream_error(session_id(), error :: term(), state()) ::
  {:retry, state()} | {:continue, state()} | {:stop, reason :: term(), state()}

Handle media stream errors.

Parameters

  • session_id - Session identifier
  • error - Error details
  • state - Current handler state

Returns

  • {:retry, state} - Retry the operation
  • {:continue, state} - Continue despite error
  • {:stop, reason, state} - Stop the stream

handle_stream_start(session_id, direction, state)

(optional)
@callback handle_stream_start(
  session_id(),
  direction :: :inbound | :outbound | :bidirectional,
  state()
) ::
  media_action()
  | {media_action(), state()}
  | {[media_action()], state()}
  | {:noreply, state()}

Handle media stream start.

Called when media stream is about to start. Can return media actions to execute.

Parameters

  • session_id - Session identifier
  • direction - :inbound, :outbound, or :bidirectional
  • state - Current handler state

Returns

  • media_action - Single action to execute
  • {media_action, state} - Action with state update
  • {[media_action], state} - Multiple actions
  • {:noreply, state} - No action

Example

@impl true
def handle_stream_start(_session_id, :inbound, state) do
  # Play welcome message
  {{:play, "/audio/welcome.wav"}, state}
end

handle_stream_stop(session_id, reason, state)

(optional)
@callback handle_stream_stop(session_id(), reason :: term(), state()) :: {:ok, state()}

Handle media stream stop.

Called when media stream stops.

Parameters

  • session_id - Session identifier
  • reason - Stop reason
  • state - Current handler state

Returns

  • {:ok, state} - Acknowledged

init(args)

@callback init(args :: term()) :: {:ok, state()} | {:stop, reason :: term()}

Initialize the media handler.

Called when a new media session starts. This happens when MediaSession is started for a dialog.

Parameters

  • args - Arguments passed when starting the handler

Returns

  • {:ok, state} - Initialize with the given state
  • {:stop, reason} - Prevent the handler from starting

Example

@impl true
def init(args) do
  {:ok, %{
    preferred_codec: :opus,
    quality_threshold: 5.0,
    play_queue: []
  }}
end