Packages
parrot_platform
0.0.1-alpha.2
Elixir libraries and OTP behaviours for building telecom applications with SIP protocol and media handling
Current section
Files
Jump to
Current section
Files
lib/parrot/media_handler.ex
defmodule Parrot.MediaHandler do
@moduledoc """
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
```elixir
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:
```elixir
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
"""
@typedoc "Handler state - can be any term"
@type state :: term()
@typedoc "Media session ID"
@type session_id :: String.t()
@typedoc "SDP direction"
@type direction :: :inbound | :outbound
@typedoc "Codec atom"
@type codec :: :pcmu | :pcma | :opus | atom()
@typedoc """
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
"""
@type media_action ::
{:play, file_path :: String.t()}
| {:play, file_path :: String.t(), opts :: keyword()}
| :stop
| :pause
| :resume
| {:set_codec, codec()}
| :noreply
# Session Lifecycle Callbacks
@doc """
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
"""
@callback init(args :: term()) :: {:ok, state} | {:stop, reason :: term()}
@doc """
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
"""
@callback handle_session_start(session_id, opts :: keyword(), state) ::
{:ok, state} | {:error, reason :: term(), state}
@doc """
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
"""
@callback handle_session_stop(session_id, reason :: term(), state) :: {:ok, state}
# SDP Negotiation Callbacks
@doc """
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
"""
@callback handle_offer(sdp :: String.t(), direction, state) ::
{:ok, modified_sdp :: String.t(), state}
| {:reject, reason :: term(), state}
| {:noreply, state}
@doc """
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
"""
@callback handle_answer(sdp :: String.t(), direction, state) ::
{:ok, modified_sdp :: String.t(), state}
| {:reject, reason :: term(), state}
| {:noreply, state}
@doc """
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
"""
@callback handle_codec_negotiation(
offered_codecs :: [codec()],
supported_codecs :: [codec()],
state
) ::
{:ok, codec(), state}
| {:ok, [codec()], state}
| {:error, :no_common_codec, state}
@doc """
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
"""
@callback handle_negotiation_complete(
local_sdp :: String.t(),
remote_sdp :: String.t(),
selected_codec :: codec(),
state
) ::
{:ok, state} | {:error, reason :: term(), state}
# Media Stream Callbacks
@doc """
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
"""
@callback handle_stream_start(
session_id,
direction :: :inbound | :outbound | :bidirectional,
state
) ::
media_action()
| {media_action(), state}
| {[media_action()], state}
| {:noreply, state}
@doc """
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
"""
@callback handle_stream_stop(session_id, reason :: term(), state) :: {:ok, state}
@doc """
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
"""
@callback handle_stream_error(session_id, error :: term(), state) ::
{:retry, state} | {:continue, state} | {:stop, reason :: term(), state}
# Media Control Callbacks
@doc """
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
"""
@callback handle_play_complete(file_path :: String.t(), state) ::
media_action() | {media_action(), state} | {:noreply, state}
@doc """
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
"""
@callback handle_media_request(request :: term(), state) ::
media_action() | {media_action(), state} | {:error, reason :: term(), state}
# Optional callbacks - all except init
@optional_callbacks [
handle_session_start: 3,
handle_session_stop: 3,
handle_offer: 3,
handle_answer: 3,
handle_codec_negotiation: 3,
handle_negotiation_complete: 4,
handle_stream_start: 3,
handle_stream_stop: 3,
handle_stream_error: 3,
handle_play_complete: 2,
handle_media_request: 2
]
end