Phoenix LiveView: Build Twitch Without Writing JavaScript
Tags: elixir
Published: 2019-10-31 20:21:14

Phoenix LiveView is a new experiment that allows developers to build rich, real-time user experiences with server-rendered HTML. If you’re not familiar with Phoenix, it’s the fully-featured web framework for the Elixir programming language. At Mux we use Phoenix and Elixir to power our API. I decided to start playing around with LiveView to see what it’s capable of. The idea I had for an example app is Snitch, it’s like “Twitch,” but for snitches (put away your checkbooks potential investors). Under the hood, of course, we’re using <a href="https://mux.com/live/" target="_blank">Mux Live Streaming</a>.

From the user’s perspective, first you create a “channel”. When that channel is created, Snitch will give you RTMP streaming credentials (just like Twitch does). As the user, you enter those streaming credentials into your mobile app or broadcast software and start streaming.

Right here we have the perfect test case for LiveView. In the UI we show the user the streaming credentials and are now waiting for them to start streaming. Mux is going to <a href="https://docs.mux.com/docs/webhooks#section-live-stream-events" target="_blank">send webhooks</a> to our server when relevant events happen. For example:

- `video.live_stream.connected`
- `video.live_stream.recording`
- `video.live_stream.active`
- `video.live_stream.disconnected`

In a typical web application, without LiveView, common solutions are to either use websockets to push new data to client applications or have those applications poll the server. About every second or so the browser would send a request to the server to get the updated data. **But now with LiveView when a webhook hits the server we can re-render on the server-side and push those changes to the client.**


## Using LiveView to Handle Webhooks

The first step is to follow the instructions on the Installation page to add LiveView to your Phoenix application. This includes adding the dependency, exposing the WebSocket route and the `phoenix_live_view` JavaScript package for the client-side.

After following the installation instructions, let’s add a route for the Mux Webhooks:


```elixir
    scope "/", SnitchWeb do
      pipe_through :api
      post "/webhooks/mux", WebhookController, :mux
    end
```

Then, in the Mux UI we can add this as our webhooks route. For local development I’m using ngrok to receive webhooks on my localhost server.




The webhook controller is going to receive the payload and update the “channel” in our database by calling `Snitch.Channels.update_channel` . Let’s look at the `update_channel/2` function:


```elixir
# lib/snitch/channels.ex 
def update_channel(%Channel{} = channel, attrs) do
  channel
  |> Channel.changeset(attrs)
  |> Repo.update()
  |> notify_subs()
end
```

`notify_subs/1` is the new function we are going to call when a channel gets updated. This is where the LiveView magic happens.


```elixir
# lib/snitch/channels.ex 
def notify_subs({:ok, channel}) do
  Phoenix.PubSub.broadcast(Snitch.PubSub, "channel-updated:#{channel.id}", channel)
  {:ok, channel}
end
```

This function is going to broadcast a message so that subscribers can react to this change. More on that shortly.

Now let’s update the controller and tell the controller to render with LiveView:

```elixir
# lib/snitch_web/controllers/channel_controller.ex 
Phoenix.LiveView.Controller.live_render(conn, SnitchWeb.LiveChannelView,
  session: %{channel: channel}
)
```

And let’s create `SnitchWeb.LiveChannelView`. When we call `notify_subs()` up above, this `LiveChannelView` is the code that needs to subscribe and push an update to the client.

```elixir
defmodule SnitchWeb.LiveChannelView do
  use Phoenix.LiveView

  #
  # When the controller calls live_render/3 this mount/2 function will get called
  # after the mount/2 function finishes then the render/1 function will get called
  # with the assigns
  #
  def mount(session, socket) do
    channel = session[:channel]
    if connected?(socket), do: SnitchWeb.Endpoint.subscribe("channel-updated:#{channel.id}")

    {
      :ok,
      set_assigns(channel, socket)
    }
  end

  def render(%{playback_url: nil} = assigns), 
    do: SnitchWeb.ChannelView.render("show.html", assigns)

  def render(assigns), do: SnitchWeb.ChannelView.render("show_active.html", assigns)

  #
  # Since the mount/2 function called "subscribe" to with the identifier
  # "channel-updated:#{channel.id}" then anytime data is broadcast this
  # handle_info/2 function will run and we have the power to set new values
  # with set_assigns/2
  #
  # After we assign new values, the render/1 function will get called with the
  # new assigns
  #
  def handle_info(channel, socket) do
    {
      :noreply,
      set_assigns(channel, socket)
    }
  end

  def set_assigns(channel, socket) do
    playback_url = Snitch.Channels.playback_url_for_channel(channel)

    socket
    |> assign(name: channel.name)
    |> assign(status: channel.mux_resource["status"])
    |> assign(connected: channel.mux_resource["connected"])
    |> assign(stream_key: channel.stream_key)
    |> assign(playback_url: playback_url)
  end
end
```

To summarize what’s happening above:

- `live_render/3` will invoke `mount/2`
- `mount/2` will subscribe using an identifier (`"channel-updated:#{channel.id}"`) and set_assigns for the view
- `render/1` will get called with the assigns
- anytime somewhere else in the app broadcasts to (`"channel-updated:#{channel.id}"`), this view is going to call `handle_info/2` and that gives us the opportunity to use `set_assigns` again to update the assigns and re-render the template
- re-renders auto-magically get pushed to the client over a websocket and the client updates the dom

The only difference in the `show` and `show_active` templates that we use in LiveChannelView is that instead of `eex` extension we use the `leex` extension which stands for live embedded elixir.

Here is a webapp where this is currently deployed at <a href="https://snitch.world" target="_blank">snitch.world</a> The full code is up here <a href="https://github.com/dylanjha/snitch">on github</a>. You can clone it and run it yourself. You’ll also need to sign up for a free account with Mux to get an API key. Feel free to reach out if you have any questions!

## Demo

{% youtube 3IQlN_Ax6mg %}