Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building Video Chat with Elixir & Phoenix

anildigital
September 06, 2018

Building Video Chat with Elixir & Phoenix

In this talk, I shared my experience and learnings about how I built a production-grade video chat system with Elixir and Phoenix.

Conference - ElixirConf® Bellevue, WA, September 4-7

anildigital

September 06, 2018
Tweet

More Decks by anildigital

Other Decks in Programming

Transcript

  1. Video data send Video data send Video data send Video

    data send WebRTC peer to peer Video data send
  2. Video data send Video data send Video data send Video

    data send Video data send WebRTC peer to peer 212.172.11.23 172.11.0.44 192.2.2.55 202.168.1.1
  3. Why Elixir & Phoenix? • Elixir • OTP features -

    GenServer, Agent, GenEvent, GenStage, Supervisor • Phoenix • Phoenix for channels (signalling), web app basics • Authentication • Libraries
  4. Why Elixir & Phoenix? • Actor model • Battle tested

    OTP abstractions • Fan out • Fault Tolerant • Soft realtime
  5. Using Janus RESTful/HTTP APIs • POST /janus (to create Janus

    Session) • POST Session (attach plugin to session) • POST Plugin (for other requests) • GET Session (for listening for events)
  6. Janus Video Room Events • slowlink • configured • talking

    (true/false) • publishers • leaving • unpublished • webrtcup • media (true/false) • hangup •
  7. Phoenix application Session GenEvent Handler Plugin GenEvent Handler Janus WebRTC

    Gateway Server Elixir Janus Client Session Plugin State & GenEvent Manager State & GenEvent Manager HTTP Util HTTP calls Room create (returns Session Agent PID & Plugin Agent PID) Agent Agent
  8. defmodule Janus.Session do
 … … def start(url) do case post(url,

    %{janus: :create}) do {:ok, body} -> id = body.data.id {:ok, event_manager} = GenEvent.start_link() session = %Janus.Session{ id: id, base_url: "#{url}/#{id}", event_manager: event_manager } Agent.start(fn -> session end) v -> v end end
  9. defmodule Janus.Session do
 … … def start(url) do case post(url,

    %{janus: :create}) do {:ok, body} -> id = body.data.id {:ok, event_manager} = GenEvent.start_link() session = %Janus.Session{ id: id, base_url: "#{url}/#{id}", event_manager: event_manager } Agent.start(fn -> session end) v -> v end end
  10. defmodule Janus.Session do
 … … def start(url) do case post(url,

    %{janus: :create}) do {:ok, body} -> id = body.data.id {:ok, event_manager} = GenEvent.start_link() session = %Janus.Session{ id: id, base_url: "#{url}/#{id}", event_manager: event_manager } Agent.start(fn -> session end) v -> v end end
  11. defmodule Janus.Session do
 … def attach_plugin(pid, id) do base_url =

    Agent.get(pid, &(&1.base_url)) v = case post(base_url, %{janus: :attach, plugin: id}) do {:ok, body} -> id = body.data.id {:ok, event_manager} = GenEvent.start_link() plugin = %Janus.Plugin{ id: id, base_url: "#{base_url}/#{id}", event_manager: event_manager } {:ok, plugin_pid} = Agent.start(fn -> plugin end) Agent.update pid, fn(session) -> new_handles = Map.put(session.handles, id, plugin_pid) %{ session | handles: new_handles} end
  12. defmodule Janus.Session do
 … def attach_plugin(pid, id) do base_url =

    Agent.get(pid, &(&1.base_url)) v = case post(base_url, %{janus: :attach, plugin: id}) do {:ok, body} -> id = body.data.id {:ok, event_manager} = GenEvent.start_link() plugin = %Janus.Plugin{ id: id, base_url: "#{base_url}/#{id}", event_manager: event_manager } {:ok, plugin_pid} = Agent.start(fn -> plugin end) Agent.update pid, fn(session) -> new_handles = Map.put(session.handles, id, plugin_pid) %{ session | handles: new_handles} end
  13. defmodule Janus.Session do
 … def attach_plugin(pid, id) do base_url =

    Agent.get(pid, &(&1.base_url)) v = case post(base_url, %{janus: :attach, plugin: id}) do {:ok, body} -> id = body.data.id {:ok, event_manager} = GenEvent.start_link() plugin = %Janus.Plugin{ id: id, base_url: "#{base_url}/#{id}", event_manager: event_manager } {:ok, plugin_pid} = Agent.start(fn -> plugin end) Agent.update pid, fn(session) -> new_handles = Map.put(session.handles, id, plugin_pid) %{ session | handles: new_handles} end
  14. defmodule Janus.Session do
 … def attach_plugin(pid, id) do base_url =

    Agent.get(pid, &(&1.base_url)) v = case post(base_url, %{janus: :attach, plugin: id}) do {:ok, body} -> id = body.data.id {:ok, event_manager} = GenEvent.start_link() plugin = %Janus.Plugin{ id: id, base_url: "#{base_url}/#{id}", event_manager: event_manager } {:ok, plugin_pid} = Agent.start(fn -> plugin end) Agent.update pid, fn(session) -> new_handles = Map.put(session.handles, id, plugin_pid) %{ session | handles: new_handles} end
  15. defmodule Janus.Session do
 … def add_handler(session, handler, args) do Agent.get

    session, &(GenEvent.add_handler(&1.event_manager, handler, args)) end
  16. defmodule Janus.Plugin do
 … def add_handler(plugin, handler, args) do Agent.get

    plugin, &(GenEvent.add_handler(&1.event_manager, handler, args)) end
  17. defp poll(pid) do session = Agent.get pid, &(&1) spawn fn

    -> case get(session.base_url) do {:ok, data} -> event_manager = session.event_manager case data do %{janus: "keepalive"} -> GenEvent.notify(event_manager, {:keepalive, pid}) %{sender: sender} -> plugin_pid = session.handles[sender] if plugin_pid do case data do %{janus: "event", plugindata: plugindata} -> jsep = data[:jsep] Agent.get plugin_pid, &(GenEvent.notify(&1.event_manager, {:event, pid, plugin_pid, plugindata.data, jsep}))
  18. defp poll(pid) do session = Agent.get pid, &(&1) spawn fn

    -> case get(session.base_url) do {:ok, data} -> event_manager = session.event_manager case data do %{janus: "keepalive"} -> GenEvent.notify(event_manager, {:keepalive, pid}) %{sender: sender} -> plugin_pid = session.handles[sender] if plugin_pid do case data do %{janus: "event", plugindata: plugindata} -> jsep = data[:jsep] Agent.get plugin_pid, &(GenEvent.notify(&1.event_manager, {:event, pid, plugin_pid, plugindata.data, jsep}))
  19. defp poll(pid) do session = Agent.get pid, &(&1) spawn fn

    -> case get(session.base_url) do {:ok, data} -> event_manager = session.event_manager case data do %{janus: "keepalive"} -> GenEvent.notify(event_manager, {:keepalive, pid}) %{sender: sender} -> plugin_pid = session.handles[sender] if plugin_pid do case data do %{janus: "event", plugindata: plugindata} -> jsep = data[:jsep] Agent.get plugin_pid, &(GenEvent.notify(&1.event_manager, {:event, pid, plugin_pid, plugindata.data, jsep}))
  20. defp poll(pid) do session = Agent.get pid, &(&1) spawn fn

    -> case get(session.base_url) do {:ok, data} -> event_manager = session.event_manager case data do %{janus: "keepalive"} -> GenEvent.notify(event_manager, {:keepalive, pid}) %{sender: sender} -> plugin_pid = session.handles[sender] if plugin_pid do case data do %{janus: "event", plugindata: plugindata} -> jsep = data[:jsep] Agent.get plugin_pid, &(GenEvent.notify(&1.event_manager, {:event, pid, plugin_pid, plugindata.data, jsep}))
  21. defp poll(pid) do session = Agent.get pid, &(&1) spawn fn

    -> case get(session.base_url) do {:ok, data} -> event_manager = session.event_manager case data do %{janus: "keepalive"} -> GenEvent.notify(event_manager, {:keepalive, pid}) %{sender: sender} -> plugin_pid = session.handles[sender] if plugin_pid do case data do %{janus: "event", plugindata: plugindata} -> jsep = data[:jsep] Agent.get plugin_pid, &(GenEvent.notify(&1.event_manager, {:event, pid, plugin_pid, plugindata.data, jsep}))
  22. Phoenix application Session GenEvent Handler Plugin GenEvent Handler Janus WebRTC

    Gateway Server Elixir Janus Client Session Plugin State & GenEvent Manager State & GenEvent Manager HTTP Util HTTP calls Room create (returns Session Agent PID & Plugin Agent PID) Agent Agent
  23. Video Room start sequence • Create Janus Room (HTTP Call)

    • Init Session Agent PID and Plugin Agent PID • Store these Agents PIDs in Cache (Cache is GenServer based) • PIDs to further communicate to Janus
  24. Video Room start sequence • Create Janus Room (HTTP Call)

    • Init Session Agent PID and Plugin Agent PID • Store these Agents PIDs in Cache (Cache is GenServer based) • PIDs to further communicate to Janus
  25. Video Room start sequence • Create Janus Room (HTTP Call)

    • Init Session Agent PID and Plugin Agent PID • Store these Agents PIDs in Cache (Cache is GenServer based) • PIDs to further communicate to Janus
  26. Video Room start sequence • Create Janus Room (HTTP Call)

    • Init Session Agent PID and Plugin Agent PID • Store these Agents PIDs in Cache (Cache is GenServer based) • PIDs to further communicate with Janus
  27. Browser Tab 1 Phoenix channel GenServer Plugin Agent Sesson Agent

    DynamicSupervisor Tab/Browser Closed elixir-janus client
  28. defmodule Janus.Session.GenServer do use GenServer def init(state) do %{url: _url,

    session: _session, handle: _handle, channel_pid: channel_pid} = state Process.monitor(channel_pid) send(self(), :setup) {:ok, state} end
  29. defmodule Janus.Session.GenServer do use GenServer def init(state) do %{url: _url,

    session: _session, handle: _handle, channel_pid: channel_pid} = state Process.monitor(channel_pid) send(self(), :setup) {:ok, state} end
  30. defmodule Janus.Session.GenServer do use GenServer … def handle_info({:DOWN, ref, :process,

    other_pid, _reason}, state) do %{url: _url, session: session, handle: handle, channel_pid: _channel_pid} = state cleanup(state) {:noreply, state} end def cleanup(state) do %{url: _url, session: session, handle: handle, channel_pid: _channel_pid} = state VideoroomCall.stop(session, handle) Process.exit(self(), :kill) state end
  31. defmodule Janus.Session.GenServer do use GenServer … def handle_info({:DOWN, ref, :process,

    other_pid, _reason}, state) do %{url: _url, session: session, handle: handle, channel_pid: _channel_pid} = state cleanup(state) {:noreply, state} end def cleanup(state) do %{url: _url, session: session, handle: handle, channel_pid: _channel_pid} = state VideoroomCall.stop(session, handle) Process.exit(self(), :kill) state end
  32. elixir-janus client Browser Tab 1 Phoenix channel GenServer Plugin Agent

    Sesson Agent Phoenix channel receives DOWN message DynamicSupervisor
  33. elixir-janus client Browser Tab 1 Phoenix channel GenServer Plugin Agent

    Sesson Agent User starts call again DynamicSupervisor
  34. defmodule SkatterWeb.SkatterRoomChannel do use SkatterWeb, :channel … defp init_janus_room(jsep, room_id,

    recording) do room = Rooms.find_by_id(room_id) {:ok, session_server} = DynamicSupervisor.start_child(Janus.Supervisor,
 Janus.Session.GenServer.child_spec([{:channel_pid, self()}]))
 ref = Process.monitor(session_server)
 
 {session, plugin_pid} = Janus.Session.GenServer.start_session(session_server, room_name) …
  35. defmodule SkatterWeb.SkatterRoomChannel do use SkatterWeb, :channel … defp init_janus_room(jsep, room_id,

    recording) do room = Rooms.find_by_id(room_id) {:ok, session_server} = DynamicSupervisor.start_child(Janus.Supervisor,
 Janus.Session.GenServer.child_spec([{:channel_pid, self()}]))
 ref = Process.monitor(session_server)
 
 {session, plugin_pid} = Janus.Session.GenServer.start_session(session_server, room_name) …
  36. defmodule SkatterWeb.SkatterRoomChannel do use SkatterWeb, :channel … defp init_janus_room(jsep, room_id,

    recording) do room = Rooms.find_by_id(room_id) {:ok, session_server} = DynamicSupervisor.start_child(Janus.Supervisor,
 Janus.Session.GenServer.child_spec([{:channel_pid, self()}]))
 ref = Process.monitor(session_server)
 
 {session, plugin_pid} = Janus.Session.GenServer.start_session(session_server, room_name) …
  37. defmodule SkatterWeb.SkatterRoomChannel do use SkatterWeb, :channel … def handle_info({:DOWN, ref,

    :process, _pid, _reason}, socket) do room_name = get_room_name(socket) StandupsWeb.Endpoint.broadcast(room_name, "data", %{ type: "stop_call"}) {:noreply, socket} end …
  38. defmodule SkatterWeb.SkatterRoomChannel do use SkatterWeb, :channel … def handle_info({:DOWN, ref,

    :process, _pid, _reason}, socket) do room_name = get_room_name(socket) StandupsWeb.Endpoint.broadcast(room_name, "data", %{ type: "stop_call"}) {:noreply, socket} end …
  39. defmodule SkatterWeb.SkatterRoomChannel do use SkatterWeb, :channel … def handle_info({:DOWN, ref,

    :process, _pid, _reason}, socket) do room_name = get_room_name(socket) SkatterWeb.Endpoint.broadcast(room_name, "data", %{ type: "stop_call"}) {:noreply, socket} end …