defmodule Specter do @moduledoc """ 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. """ alias Specter.Native @enforce_keys [:native] defstruct [:native] @typedoc """ `t:native_t/0` references are returned from the NIF, and represent state held in Rust code. """ @opaque native_t() :: Specter.Native.t() @typedoc """ `t:Specter.t/0` wraps the reference returned from `init/1`. All functions interacting with NIF state take a `t:Specter.t/0` as their first argument. """ @type t() :: %Specter{native: native_t()} @typedoc """ `t:Specter.api_t/0` represent an instantiated API managed in the NIF. """ @opaque api_t() :: String.t() @typedoc """ `t:Specter.media_engine_t/0` represents an instantiated MediaEngine managed in the NIF. """ @opaque media_engine_t() :: String.t() @typedoc """ `t:Specter.registry_t/0` represent an instantiated intercepter Registry managed in the NIF. """ @opaque registry_t() :: String.t() @typedoc """ A uri in the form `protocol:host:port`, where protocol is either `stun` or `turn`. Defaults to `stun:stun.l.google.com:19302`. """ @type ice_server() :: String.t() @typedoc """ Options for initializing RTCPeerConnections. This is set during initialization of the library, and later used when creating new connections. """ @type init_options() :: [] | [ice_servers: [ice_server()]] @doc """ Initialize the library. This registers the calling process to receive callback messages to `handle_info/2`. | param | type | default | | ------------- | ------------------ | ------- | | `ice_servers` | `list(String.t())` | `["stun:stun.l.google.com:19302"]` | ## Usage iex> {:ok, _specter} = Specter.init(ice_servers: ["stun:stun.example.com:3478"]) """ @spec init() :: {:ok, t()} @spec init(init_options()) :: {:ok, t()} | {:error, term()} def init(args \\ []) do with {:ok, native} <- Native.init(args) do {:ok, %Specter{native: native}} end end @doc """ 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"]}} """ @spec config(t()) :: {:ok, Specter.Config.t()} | {:error, term()} def config(%Specter{native: ref}), do: Native.config(ref) @doc """ 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 """ @spec media_engine_exists?(t(), media_engine_t()) :: boolean() | no_return() def media_engine_exists?(%Specter{native: ref}, media_engine) do case Native.media_engine_exists(ref, media_engine) do {:ok, value} -> value {:error, error} -> raise "Unable to determine whether media engine exists:\n#{inspect(error)}" end end @doc """ 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. | param | type | default | | -------------- | -------- | ------- | | `specter` | `t()` | | | `media_engine` | `opaque` | | | `registry` | `opaque` | | ## 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) """ @spec new_api(t(), media_engine_t(), registry_t()) :: {:ok, api_t()} | {:error, term()} def new_api(%Specter{native: ref}, media_engine, registry), do: Native.new_api(ref, media_engine, registry) @doc """ 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) """ @spec new_media_engine(t()) :: {:ok, media_engine_t()} | {:error, term()} def new_media_engine(%Specter{native: ref}), do: Native.new_media_engine(ref) @doc """ 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 """ @spec new_registry(t(), media_engine_t()) :: {:ok, registry_t()} | {:error, term()} def new_registry(%Specter{native: ref}, media_engine), do: Native.new_registry(ref, media_engine) @doc """ 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 """ @spec registry_exists?(t(), registry_t()) :: boolean() | no_return() def registry_exists?(%Specter{native: ref}, registry) do case Native.registry_exists(ref, registry) do {:ok, value} -> value {:error, error} -> raise "Unable to determine whether registry exists:\n#{inspect(error)}" end end end