Phoenix LiveView: Build Twitch Without Writing JavaScript
Dylan Jhaveri
Posted on October 31, 2019
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 invokemount/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 callhandle_info/2
and that gives us the opportunity to useset_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
Posted on October 31, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.