Specter (specter v0.5.0)

View Source

Specter is a method for managing data structures and entities provided by webrtc.rs. It is intended as a low-level library with some small set of opinions, which can composed into more complex behaviors by higher-level libraries and applications.

Key points

Specter wraps webrtc.rs, which heavily utilizes async Rust. For this reason, many functions cannot be automatically awaited by the caller—the NIF functions send messages across channels to separate threads managed by Rust, which send messages back to Elixir that can be caught by receive or handle_info.

Usage

A process initializes Specter via the init/1 function, which registers the current process for callbacks that may be triggered via webrtc entities.

iex> ## Initialize the library. Register messages to the current pid.
iex> {:ok, specter} = Specter.init(ice_servers: ["stun:stun.l.google.com:19302"])
...>
iex> ## Create a peer connection's dependencies
iex> {:ok, media_engine} = Specter.new_media_engine(specter)
iex> {:ok, registry} = Specter.new_registry(specter, media_engine)
...>
iex> Specter.media_engine_exists?(specter, media_engine)
true
iex> Specter.registry_exists?(specter, registry)
true
...>
iex> {:ok, api} = Specter.new_api(specter, media_engine, registry)
...>
iex> Specter.media_engine_exists?(specter, media_engine)
false
iex> Specter.registry_exists?(specter, registry)
false
...>
iex> ## Create a peer connection
iex> {:ok, pc_1} = Specter.PeerConnection.new(specter, api)
iex> assert_receive {:peer_connection_ready, ^pc_1}
iex> Specter.PeerConnection.exists?(specter, pc_1)
true
iex> ## Add a thing to be negotiated
iex> :ok = Specter.PeerConnection.create_data_channel(specter, pc_1, "data")
iex> assert_receive {:data_channel_created, ^pc_1}
...>
iex> ## Create an offer
iex> :ok = Specter.PeerConnection.create_offer(specter, pc_1)
iex> assert_receive {:offer, ^pc_1, offer}
iex> :ok = Specter.PeerConnection.set_local_description(specter, pc_1, offer)
iex> assert_receive {:ok, ^pc_1, :set_local_description}
...>
iex> ## Create a second peer connection, to answer back
iex> {:ok, media_engine} = Specter.new_media_engine(specter)
iex> {:ok, registry} = Specter.new_registry(specter, media_engine)
iex> {:ok, api} = Specter.new_api(specter, media_engine, registry)
iex> {:ok, pc_2} = Specter.PeerConnection.new(specter, api)
iex> assert_receive {:peer_connection_ready, ^pc_2}
...>
iex> ## Begin negotiating offer/answer
iex> :ok = Specter.PeerConnection.set_remote_description(specter, pc_2, offer)
iex> assert_receive {:ok, ^pc_2, :set_remote_description}
iex> :ok = Specter.PeerConnection.create_answer(specter, pc_2)
iex> assert_receive {:answer, ^pc_2, answer}
iex> :ok = Specter.PeerConnection.set_local_description(specter, pc_2, answer)
iex> assert_receive {:ok, ^pc_2, :set_local_description}
...>
iex> ## Receive ice candidates
iex> assert_receive {:ice_candidate, ^pc_1, _candidate}
iex> assert_receive {:ice_candidate, ^pc_2, _candidate}
...>
iex> ## Shut everything down
iex> Specter.PeerConnection.close(specter, pc_1)
:ok
iex> assert_receive {:peer_connection_closed, ^pc_1}
...>
iex> :ok = Specter.PeerConnection.close(specter, pc_2)
iex> assert_receive {:peer_connection_closed, ^pc_2}

Thoughts

During development of the library, it can be assumed that callers will implement handle_info/2 function heads appropriate to the underlying implementation. Once these are more solid, it would be nice to use Specter, which will inject a handle_info/2 callback, and send the messages to other callback functions defined by a behaviour. handle_ice_candidate, and so on.

Some things are returned from the NIF as UUIDs. These are declared as @opaque, to indicate that users of the library should not rely of them being in a particular format. They could change later to be references, for instance.

Summary

Types

Specter.api_t/0 represent an instantiated API managed in the NIF.

A uri in the form protocol:host:port, where protocol is either stun or turn.

Options for initializing RTCPeerConnections. This is set during initialization of the library, and later used when creating new connections.

Specter.media_engine_t/0 represents an instantiated MediaEngine managed in the NIF.

native_t/0 references are returned from the NIF, and represent state held in Rust code.

Specter.registry_t/0 represent an instantiated intercepter Registry managed in the NIF.

t()

Specter.t/0 wraps the reference returned from init/1. All functions interacting with NIF state take a Specter.t/0 as their first argument.

Functions

Returns the current configuration for the initialized NIF.

Initialize the library. This registers the calling process to receive callback messages to handle_info/2.

Returns true or false, depending on whether the media engine is available for consumption, i.e. is initialized and has not been used by a function that takes ownership of it.

An APIBuilder is used to create RTCPeerConnections. This accepts as parameters the output of init/1, new_media_enine/1, and new_registry/2.

Creates a MediaEngine to be configured and used by later function calls. Codecs and other high level configuration are done on instances of MediaEngines. A MediaEngine is combined with a Registry in an entity called an APIBuilder, which is then used to create RTCPeerConnections.

Creates an intercepter registry. This is a user configurable RTP/RTCP pipeline, and provides features such as NACKs and RTCP Reports. A registry must be created for each peer connection.

Returns true or false, depending on whether the registry is available for consumption, i.e. is initialized and has not been used by a function that takes ownership of it.

Types

api_t()

@opaque api_t()

Specter.api_t/0 represent an instantiated API managed in the NIF.

ice_server()

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

A uri in the form protocol:host:port, where protocol is either stun or turn.

Defaults to stun:stun.l.google.com:19302.

init_options()

@type init_options() :: [] | [{:ice_servers, [ice_server()]}]

Options for initializing RTCPeerConnections. This is set during initialization of the library, and later used when creating new connections.

media_engine_t()

@opaque media_engine_t()

Specter.media_engine_t/0 represents an instantiated MediaEngine managed in the NIF.

native_t()

@opaque native_t()

native_t/0 references are returned from the NIF, and represent state held in Rust code.

registry_t()

@opaque registry_t()

Specter.registry_t/0 represent an instantiated intercepter Registry managed in the NIF.

t()

@type t() :: %Specter{native: native_t()}

Specter.t/0 wraps the reference returned from init/1. All functions interacting with NIF state take a Specter.t/0 as their first argument.

Functions

config(specter)

@spec config(t()) :: {:ok, Specter.Config.t()} | {:error, term()}

Returns the current configuration for the initialized NIF.

Usage

iex> {:ok, specter} = Specter.init(ice_servers: ["stun:stun.example.com:3478"])
iex> Specter.config(specter)
{:ok, %Specter.Config{ice_servers: ["stun:stun.example.com:3478"]}}

init(args \\ [])

@spec init(init_options()) :: {:ok, t()} | {:error, term()}

Initialize the library. This registers the calling process to receive callback messages to handle_info/2.

paramtypedefault
ice_serverslist(String.t())["stun:stun.l.google.com:19302"]

Usage

iex> {:ok, _specter} = Specter.init(ice_servers: ["stun:stun.example.com:3478"])

media_engine_exists?(specter, media_engine)

@spec media_engine_exists?(t(), media_engine_t()) :: boolean() | no_return()

Returns true or false, depending on whether the media engine is available for consumption, i.e. is initialized and has not been used by a function that takes ownership of it.

Usage

iex> {:ok, specter} = Specter.init(ice_servers: ["stun:stun.l.google.com:19302"])
iex> {:ok, media_engine} = Specter.new_media_engine(specter)
iex> Specter.media_engine_exists?(specter, media_engine)
true

iex> {:ok, specter} = Specter.init(ice_servers: ["stun:stun.l.google.com:19302"])
iex> Specter.media_engine_exists?(specter, UUID.uuid4())
false

new_api(specter, media_engine, registry)

@spec new_api(t(), media_engine_t(), registry_t()) ::
  {:ok, api_t()} | {:error, term()}

An APIBuilder is used to create RTCPeerConnections. This accepts as parameters the output of init/1, new_media_enine/1, and new_registry/2.

Note that this takes ownership of both the media engine and the registry, effectively consuming them.

paramtypedefault
spectert()
media_engineopaque
registryopaque

Usage

iex> {:ok, specter} = Specter.init(ice_servers: ["stun:stun.l.google.com:19302"])
iex> {:ok, media_engine} = Specter.new_media_engine(specter)
iex> {:ok, registry} = Specter.new_registry(specter, media_engine)
iex> {:ok, _api} = Specter.new_api(specter, media_engine, registry)

new_media_engine(specter)

@spec new_media_engine(t()) :: {:ok, media_engine_t()} | {:error, term()}

Creates a MediaEngine to be configured and used by later function calls. Codecs and other high level configuration are done on instances of MediaEngines. A MediaEngine is combined with a Registry in an entity called an APIBuilder, which is then used to create RTCPeerConnections.

Usage

iex> {:ok, specter} = Specter.init(ice_servers: ["stun:stun.l.google.com:19302"])
iex> {:ok, _media_engine} = Specter.new_media_engine(specter)

new_registry(specter, media_engine)

@spec new_registry(t(), media_engine_t()) :: {:ok, registry_t()} | {:error, term()}

Creates an intercepter registry. This is a user configurable RTP/RTCP pipeline, and provides features such as NACKs and RTCP Reports. A registry must be created for each peer connection.

The registry may be combined with a MediaEngine in an API (consuming both). The API instance is then used to create RTCPeerConnections.

Note that creating a registry does not take ownership of the media engine.

Usage

iex> {:ok, specter} = Specter.init(ice_servers: ["stun:stun.l.google.com:19302"])
iex> {:ok, media_engine} = Specter.new_media_engine(specter)
iex> {:ok, _registry} = Specter.new_registry(specter, media_engine)
...>
iex> Specter.media_engine_exists?(specter, media_engine)
true

registry_exists?(specter, registry)

@spec registry_exists?(t(), registry_t()) :: boolean() | no_return()

Returns true or false, depending on whether the registry is available for consumption, i.e. is initialized and has not been used by a function that takes ownership of it.

Usage

iex> {:ok, specter} = Specter.init(ice_servers: ["stun:stun.l.google.com:19302"])
iex> {:ok, media_engine} = Specter.new_media_engine(specter)
iex> {:ok, registry} = Specter.new_registry(specter, media_engine)
iex> Specter.registry_exists?(specter, registry)
true

iex> {:ok, specter} = Specter.init(ice_servers: ["stun:stun.l.google.com:19302"])
iex> Specter.registry_exists?(specter, UUID.uuid4())
false