Warner Bros. Discovery is committed to putting viewer experience first. Learn how they're doing it with Mux.

Skip to content

How to build your own live streaming app with Mux Video

Mux Video provides simple but powerful APIs which make it easy for you to build beautiful video into your application. This guide will show you step-by-step how to build your own live streaming app using the Mux Video API. The new app we’ll build today will show users live streams whenever we’re broadcasting, and show an archive of past streams which users can view.

LinkWhy a live streaming app?

The rise of live video experiences on the web has been prolific - the barrier to entry is now sufficiently low that anyone with a phone or computer can create a high quality live streaming experience - no longer do we need complex playout suites and TV grade broadcast equipment to get our content in front of a global audience. Live video allows you to engage intimately with your audience, and allows you to build engaging interactive experiences like those seen in HQ Trivia, Twitch, or Crowdcast.

LinkComponents of our live streaming app

We’ll need to build a web application using our video streaming SDK to interact with the Mux Video APIs to ingest streams and to listen for callbacks from Mux about the state of assets in the platform. We’ll also build a simple front end to our application.

To prototype our application we’ll be using glitch.com. Glitch is a great tool for prototyping single page web applications. You can get started by building a new application at glitch.com, but we’d recommend you start by remixing the application we’ve already built for you - just click the “remix” button in the top right corner.

Our web application will be a fairly simple Node.js app. We’ll be using the following technologies to build it:

  • Server framework - Express
  • Client side framework - Mithril.js
  • Event based client-server communication - Socket.io

Because we’re building in Node.js, we’re able to use the Mux Node SDK, which makes interacting with the Mux Video APIs easy. All we need to do to get started is include the Mux SDK as a dependency. In Glitch, we can just edit our package.json file, and add the Mux SDK to our dependencies. If you’re building your own app, you can install SDK directly from NPM with npm install mux-node.

Server-side dependencies

The full list of dependencies you’ll need on the server side are as follows. You should add these to your package.json on Glitch and it will automatically install all the packages you need:

json
"dependencies": { "@mux/mux-node": "^2.0.0", "basic-auth": "^2.0.1", "express": "^4.16.3", "socket.io": "^2.2.0" }

Important concepts

Before we get too deep into building our application, let’s take a quick look at an overview of the terminology we’ll be using as we build our application.

LinkCreating a new live stream

The first thing that our application will need to be able to do is create a new live stream. We’ll want to do this on the server side of our application to protect our Mux credentials from being stolen.

When we create a new live stream, we’ll be provided with a Stream Key - we’ll need this when we’re pushing a stream into Mux Video. For now we’ll just log the stream key to the console on the server when our application starts up, so you can copy and paste it into your favorite streaming application.

So let’s put together the initialization steps of our application. We’ll create a new file, server.js, do some basic application wiring, and create ourselves a new stream key.

Note: We’ve removed some of the boiler plate to save space and make this code more readable in a blog format - you can find the full code on Glitch here.

javascript
let STREAM; // Reads a state file looking for an existing Live Stream, if it can't find one, // creates a new one, saving the new live stream to our state file and global // STREAM variable. const initialize = async () => { try { const stateFile = await readFile(stateFilePath, 'utf8'); STREAM = JSON.parse(stateFile); console.log('Found an existing stream! Fetching updated data.'); STREAM = await Video.LiveStreams.get(STREAM.id); } catch (err) { console.log('No stream found, creating a new one.'); STREAM = await createLiveStream(); await writeFile(stateFilePath, JSON.stringify(STREAM)); } return STREAM; }; // Starts the HTTP listener for our application. // Note: glitch helpfully remaps HTTP 80 and 443 to process.env.PORT initialize().then((stream) => { const listener = http.listen(process.env.PORT || 4000, function () { console.log('Your app is listening on port ' + listener.address().port); console.log('HERE ARE YOUR STREAM DETAILS, KEEP THEM SECRET!'); console.log(`Stream Key: ${stream.stream_key}`); }); }); // Creates a new Live Stream so we can get a Stream Key const createLiveStream = async () => { if (!process.env.MUX_TOKEN_ID || !process.env.MUX_TOKEN_SECRET) { console.error("It looks like you haven't set up your Mux token in the .env file yet."); return; } // Create a new Live Stream! return await Video.LiveStreams.create({ playback_policy: 'public', reconnect_window: 10, new_asset_settings: { playback_policy: 'public' }, }); };

We’ll also want to store the stream object so that we can re-use it without having to create a new stream every time the application starts. As you can see, to do this we’ll just write a JSON file on local disk under the .data directory. If you’re using Glitch, that file will persist cleanly between runs of your application.

LinkDriving our application via webhooks

Our application will be driven by webhooks from Mux Video. Webhooks are a great way to drive applications without resorting to polling the other APIs. Right now we could realistically get away with just polling the API to check for active live streams, but this won’t scale well when we have hundreds or thousands of broadcasters all using our new app.

Because we’ll be listening for callbacks from Mux on the server side, we’ll need a way to communicate that data back to our front-end application too. To do this without polling, we’ll be using web sockets so when we receive a callback about a change in state of a video stream from Mux, we can ask our client application to re-draw itself.

To receive callbacks, we’ll need to configure the endpoint of our application in the Mux Dashboard. First we need to log into our account, and then hover over the Settings gear on the bottom right, and click “Webhooks.” Next we’ll need to create a new webhook for the appropriate environment setting the “URL to notify” as the endpoint of where the application will be running. We’re also going to be using HTTP basic authentication to validate that our callbacks are from Mux.

Mux documents the type of webhooks it’ll send for a live stream here. The two callbacks we’re interested in for our application are:

- video.live_stream.active
- video.live_stream.idle

When we see a video.live_stream.active event, we’ll want to present the video player to the user and start playback, and when we see video.live_stream.idle event, we’ll want to destroy and hide the player. There’s a lot of extra information embedded in the callbacks we’ll receive, but we’re only interested in a few fields, so we’ll be filtering out most of the callback on the server side. Here’s what we’re left with:

1. The status of the live stream.
2. The public playback ID (We’ll just assume the first entry in playback_ids is a public stream.) We’ll need this to be able to construct the playback URL for the live stream in Mux.
3. The recent_asset_ids - Every live stream broadcast through Mux is automatically archived so that you can watch it later - we’ll use these IDs later in our application.

In my case, I’ve set the callback endpoint to: https://muxer:muxology@tubelet.glitch.me/mux-hook (you’ll need to use your own project name if you clone the Glitch project.) Let’s build a handler for those callbacks. We’ll create a new route in our application listening for POSTs:

Note: We haven’t included the authentication code here to make this more readable. You can view the full file and project here on Glitch.

javascript
// API which Listens for callbacks from Mux app.post('/mux-hook', auth, function (req, res) { STREAM.status = req.body.data.status; switch (req.body.type) { // When a stream goes idle, we want to capture the automatically created // asset IDs, so we can let people watch the on-demand copies of our live streams case 'video.live_stream.idle': STREAM['recent_asset_ids'] = req.body.data['recent_asset_ids']; // We deliberately don't break; here // When a Live Stream is active or idle, we want to push a new event down our // web socket connection to our frontend, so that it update and display or hide // the live stream. case 'video.live_stream.active': io.emit('stream_update', publicStreamDetails(STREAM)); break; default: // Relaxing. } res.status(200).send('Thanks, Mux!'); }); // Lazy way to find a public playback ID (Just returns the first...) const getPlaybackId = stream => stream\['playback_ids'\][0].id; // Gets a trimmed public stream details from a stream for use on the client side const publicStreamDetails = stream => ({ status: stream.status, playbackId: getPlaybackId(stream), recentAssets: stream['recent_asset_ids'], })

We’ve almost got everything we need to drive the server side of our application, however, we want new viewers to be able to join the site after a stream has already started. For that we’ll need an API which can relay the playback details of a stream that’s already in progress. We’ll map this API to GET /stream and relay the appropriate playback information back to the client.

javascript
// API for getting the current live stream and its state for bootstrapping the app app.get('/stream', async (req, res) => { const stream = await Video.LiveStreams.get(STREAM.id); res.json(publicStreamDetails(stream)); });

LinkBuilding a simple front end

We’ve got everything we need on the server side for our front end to display our live streams, so let’s start building a simple front end app for our product.

First of all, we’ll need an empty HTML page which can interact with using Mithril. Let’s add a HTML page to our project that includes all our dependencies. We’ll add this in public/index.html

html
<!DOCTYPE html> <html lang="en"> <head> <title>Tubelet</title> <meta name="description" content="A simple live streaming video app, powered by Mux.com" /> <link id="favicon" rel="icon" href="https://glitch.com/edit/favicon-app.ico" type="image/x-icon" /> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="stylesheet" href="/style.css" /> <script src="https://unpkg.com/mithril/mithril.js"></script> <script src="/socket.io/socket.io.js"></script> <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script> <script src="/client.js" defer></script> <script src="https://button.glitch.me/button.js" data-style="glitch"></script> </head> <body> <h1>Loading...</h1> </body> </html>

Next up, let’s create the barebones of the Mithril.js application we’ll be using. There’s 2 main components to it, the Video player and the main App. The video player is responsible for… well, playing a video given a playbackID. The App is responsible for:

  • Checking for an active live stream when the page is loaded
  • Listening for messages on the web sockets about starting streams
  • Adding a player to the DOM if there is an active stream
  • Triggering re-drawing of the application at appropriate times

Player

First, let’s build the code we’ll use to make a video player appear and play the Mux stream. We’ll add a new public/client.js file to our project so that our HTML page we just created above can include it. There are lots of web players out there - we’ll be using hls.js as our web player, which is a lightweight library which implements HLS playback on browsers which don’t natively support it, such as Chrome.

Again, we’ve removed some of the boiler plate to make the code more readable. You can see the full file on Glitch.

javascript
const Video = { // Substitute the playbackID into the URL so we have a HLS playlist, and a // thumbnail URL we can use in the player. src: (playbackId) => `https://stream.mux.com/${playbackId}.m3u8`, poster: (playbackId) => `https://image.mux.com/${playbackId}/thumbnail.jpg`, oncreate: (vnode) => { const sourceUrl = Video.src(vnode.attrs.playbackId); const video = vnode.dom; // If HLS.js is supported on this platform if (Hls.isSupported()) { const hls = new Hls(); hls.loadSource(sourceUrl); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, () => { video.controls = true; video.play(); }); // If the player can support HLS natively } else if (video.canPlayType('application/vnd.apple.mpegurl')) { video.src = sourceUrl; video.addEventListener('loadedmetadata', () => { video.controls = true; video.play(); }); } }, // Initialise the player muted so that we can autoplay the Live Stream view: (vnode) => { return m('video#video', { controls: false, poster: Video.poster(vnode.attrs.playbackId), muted: true, }); }, };

Application

Now let’s put together a quick application which starts playback whenever it knows there’s a stream available.

javascript
const socket = io(); const App = { stream: {}, // The public information about our stream will be stored here. oninit: async () => { // When our application starts, check if there's a stream happening. await App.getStream(); // When we receive events on the websocket, stash the stream details from the // callback, and re-draw. socket.on('stream_update', (stream) => { App.stream = stream; m.redraw(); }); }, // Hits our GET /stream endpoint. Used when the app starts up to check if there's // an active stream getStream: () => { m.request({ method: 'GET', url: '/stream', }).then((result) => { App.stream = result; }); }, // When the app is drawn or re-drawn, if there's an active stream, add the // Video component to the view. view: () => { return [ m('main', [ m('h2', { class: 'title' }, `Status: ${App.stream.status || 'loading...'}`), App.stream.status === 'active' ? m(Video, { playbackId: App.stream.playbackId }) : m('.placeholder', 'No active stream right now 😢'), ]), ]; }, };

Let’s test it!

Great! Now we’ve got everything we need to start pushing a stream to our new application.

There are lots of tools you can use to push a stream into Mux - all you need is an application which can output RTMP. We’ll show you two free and open source tools you can use to ingest a live stream.

OBS

OBS is an open source live streaming application which is popular with Twitch streamers and organizations alike. It has a wide variety of features and can be used to create very professional looking live streams. (We use OBS to stream the Demuxed live streams on Twitch!)

Setting up OBS to stream to Mux is really simple. Download OBS from the official website, load it up, and setup the output stream as follows:

  • Settings => Stream
  • Stream Type: Custom Streaming Server
  • URL: rtmp://global-live.mux.com:5222/app
  • Stream Key: <Copy and paste this from your application when it starts up>

I’ve even turned the steps into a handy GIF below!

If everything is hooked up correctly, we should be able to watch our live stream on our web application. Let’s take a look! All we need to do is hit “Start Streaming” in OBS and load up our web application in our favorite web browser.

Great, it works. Let’s take a quick look at another easy way we can push content into our live streaming platform.

FFmpeg

FFmpeg is an industry standard command line tool for interacting with, modifying, encoding, and streaming video. We can also just use FFmpeg to stream content into Mux easily. Just use the command below, substituting in a local H.264 encoded file path and your stream key:

bash
ffmpeg -re -i path/to/h264/file.mp4 -acodec copy -vcodec copy -b:v 2000k -r 30 -f flv rtmp://global-live.mux.com:5222/app/SECRET-STREAM-KEY-HERE

LinkArchived videos

One of the best things about Mux Live is that it automatically creates VOD assets every time you end a live stream. The live stream API even gives you back the asset IDs of the VOD asset it creates in the callbacks you receive. This is great because it means that we can use the data we already have flowing into our application via the webhooks to drive a historic video experience.

First we’ll need to make a couple of small changes to our server.js to add a new API like we have for the current live stream so that our client application will be able to bootstrap itself when it starts up.

Updating our server

javascript
// API which Returns the 5 most recent VOD assets made from our Live Stream app.get('/recent', async (req, res) => { const recentAssetIds = STREAM['recent_asset_ids'] || []; // For each VOD asset we know about, get the details from Mux Video const assets = await Promise.all( recentAssetIds .reverse() .slice(0, 5) .map((assetId) => Video.Assets.get(assetId).then((asset) => { return { playbackId: getPlaybackId(asset), status: asset.status, createdAt: asset.created_at, }; }) ) ); res.json(assets); });

Client-side changes

Next, we need to update our Mithril client so that it can display our historic streams. We’ll add a new component in client.js called ArchivePreview. These ArchivePreviews will be represented by thumbnails of the live stream.

What’s great is that we can leverage the Mux image URL API to generate thumbnail previews on the fly using just the playbackID just like we did earlier to get a HLS manifest URL for playback. What’s even cooler is that we can also request animated GIFs of the stream from Mux. We’ll use those when a user rolls their mouse over the preview.

javascript
const ArchivePreview = () => { let hovered = false; const onHover = () => { hovered = true; }; const offHover = () => { hovered = false; }; // When the mouse is hovered over the preview, flip to the animated gif! const src = (playbackId) => hovered ? `https://image.mux.com/${playbackId}/animated.gif` : `https://image.mux.com/${playbackId}/thumbnail.jpg`; return { view: (vnode) => { return m('img.archivePreview', { onmouseover: onHover, onmouseout: offHover, src: src(vnode.attrs.playbackId), }); }, }; };

We also need to update our initialization code and our web socket receiver so that when the application is loaded or when we get a message about a stream ending we request the archived streams and add them to our data model.

javascript
recentStreams: [], // Recent streams are stored here oninit: async () => { await App.getStream(); App.getRecentStreams() // When we get a message about a stream going idle, we want to trigger an update // of the recent streams. socket.on('stream_update', (stream) => { if (stream.status === 'idle') { App.getRecentStreams(); } App.stream = stream; m.redraw(); }); }, // Helper method for getting recent streams and storing the result getRecentStreams: () => { m.request({ method: 'GET', url: '/recent', }) .then((result) => { App.recentStreams = result; }) },

Now all we need to do is add a little code to our view to add a place to display our recent streams.

javascript
// View for Recent Streams m('h3', 'Recent Streams'), m('ul.recentStreams', App.recentStreams.map((asset) => ( m('li', [ m('span.time', (new Date(asset.createdAt * 1000)).toDateString()), m(ArchivePreview, { playbackId: asset.playbackId }) ]) ))),

Hopefully if we’ve got everything right, and you’ve already completed at least one stream, you’ll be able to reload your app and we should see our historic streams. Even better, we should be able to hover over the thumbnails to see them animate.

LinkNext steps: Try it yourself!

While we’ve built our stream preview, we haven’t yet built a page where you can go to watch the streams. That’s our challenge for you - have a go with the code we’ve put on Glitch and add in the archive stream page!

So what are you waiting for?! Remix our application on glitch.me, add more features, or go in a different direction and start building something new - we can’t wait to see what you’ll come up with!

No credit card required to start using Mux.