DEV Community

Cover image for Phoenix LiveView: Build Twitch Without Writing JavaScript
Dylan Jhaveri for Mux

Posted on • Edited on • Originally published at mux.com

Phoenix LiveView: Build Twitch Without Writing JavaScript

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 Mux Live Streaming.

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 send webhooks 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:

    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:

# 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.

# 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:

# 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.

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 snitch.world The full code is up here on github. 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

Top comments (0)