Video works as a stream. This means that instead of sending the entire video at once, the video is sent as a set of smaller chunks that make up the entire video. This explains the buffering when watching video on slow broadband, because it only plays the clips it receives and tries to load more.

This article is for developers who are willing to learn new technologies by building an actual project: a video streaming application with node.js as a back end and Nuxt.js as a client.

  • Node.js is a runtime for building fast and scalable applications. We’ll use it to handle video capture and streaming, generate thumbnails for videos, and provide titles and captions for videos.
  • Nuxt.js is a vue.js framework that helps us easily build server-rendered vue.js applications. We will consume our API for videos, and the application will have two views: a list of available videos and a player view for each video.

A prerequisite for

  • Understanding of HTML, CSS, JavaScript, Node/Express and Vue.
  • A text editor (such as VS Code).
  • A web browser (e.g. Chrome, Firefox).
  • Install FFmpeg on your workstation.
  • Node. Js. NVM.
  • You can get the source code on GitHub.

Set up our application

In this application, we will establish a route to make requests from the foreground.

  • videosRoute to get a list of videos and their data.
  • A route to get only one video from our list of videos.
  • streamingRouting, for streaming video.
  • captionsRouting, adding subtitles to the video we’re playing.

After our route is created, we will set up our Nuxt front end, where we will create Home and dynamic Player pages. We then ask our Videos route to populate the home page with the video data, another request to stream the video on our Player page, and the last request to provide the subtitle file used for the video.

To set up our application, we create our project directory.

mkdir streaming-app
Copy the code

Set up our server

In our streaming-app directory, we create a folder called Backend.

cd streaming-app
mkdir backend
Copy the code

In our background folder, we initialize a package.json file to store information about our server project.

cd backend
npm init -y
Copy the code

We need to install the following packages to build our application.

  • nodemonAutomatically restart our server as we make changes.
  • expressIt gives us a nice interface to handle routing.
  • corsWill allow us to make cross-source requests because our client and server will be running on different ports.

In our background directory, we create a folder assets to hold our video stream.

 mkdir assets
Copy the code

Copy an.mp4 file into assets folder and name it Video1. You can use.mp4, short video samples available on the Github Repo.

Create an app.js file to add the necessary packages for our application.

const express = require('express');
const fs = require('fs');
const cors = require('cors');
const path = require('path');
const app = express();
app.use(cors())
Copy the code

The FS module is used to easily read and write files on our server, while the PATH module provides a way to handle directories and file paths.

Now we create a./video route. When requested, it sends a video file back to the client.

// add after 'const app = express(); ' app.get('/video', (req, res) => { res.sendFile('assets/video1.mp4', { root: __dirname }); });Copy the code

This route provides the video1.mp4 video file when requested. Then we listen to our server on port 3000.

// add to end of app.js file

app.listen(5000, () => {
    console.log('Listening on port 5000!')
});
Copy the code

Add a script to the package.json file to start our server using Nodemon.


"scripts": {
    "start": "nodemon app.js"
  },
Copy the code

And then run it on your terminal.

npm run start
Copy the code

If you see Listening on Port 3000! Then the server will work properly. Navigate to http://localhost:5000/video in your browser, you should see the video is playing.

Requests to be processed by the foreground

Here are the requests we will make from the front end to the back end that we need the server to handle.

  • /videos

    Returns an array of video emulation data that will be used to populate our front endHomeList of videos on the page.
  • /video/:id/data

    Returns metadata for a single video. By our front endPlayerPage usage.
  • /video/:id

    Stream videos with the given ID. byPlayerPage usage.

Let’s create a route.

Returns simulated data for the video list

For this demo application, we will create an array of objects to hold the metadata and send it to the front end when requested. In a real application, you might read data from a database and use it to generate such an array. For the sake of simplicity, we won’t do that in this tutorial.

Create a file mockData.js in our back-end folder and populate our list of videos with metadata.

const allVideos = [
    {
        id: "tom and jerry",
        poster: 'https://image.tmdb.org/t/p/w500/fev8UFNFFYsD5q7AcYS8LyTzqwl.jpg',
        duration: '3 mins',
        name: 'Tom & Jerry'
    },
    {
        id: "soul",
        poster: 'https://image.tmdb.org/t/p/w500/kf456ZqeC45XTvo6W9pW5clYKfQ.jpg',
        duration: '4 mins',
        name: 'Soul'
    },
    {
        id: "outside the wire",
        poster: 'https://image.tmdb.org/t/p/w500/lOSdUkGQmbAl5JQ3QoHqBZUbZhC.jpg',
        duration: '2 mins',
        name: 'Outside the wire'
    },
];
module.exports = allVideos
Copy the code

As we can see from above, each object contains information about the video. Note the Poster attribute, which contains the link to the poster image for the video.

Let’s create a videos route because all of our requests to be made by the front end are prefixed with /videos.

To do this, let’s create a routes folder and add a video.js file to our /videos route. In this file, we will require Express and use the Express Router to create our route.

const express = require('express')
const router = express.Router()
Copy the code

When we go to the/Videos route, we want to get our list of videos, so let’s ask for the mockData.js file to go into our video.js file and make our request.

const express = require('express') const router = express.Router() const videos = require('.. /mockData') // get list of videos router.get('/', (req,res)=>{ res.json(videos) }) module.exports = router;Copy the code

The/VIDEOS route has now been declared to save the file and it should automatically restart the server. Once started, navigate to http://localhost:3000/videos, we array will be returned in JSON format.

Returns data for a single video

We want to be able to request a particular video from our video list. We can get a particular video data in our array by using the ID we gave it. Let’s make a request, again in our video.js file.


// make request for a particular video
router.get('/:id/data', (req,res)=> {
    const id = parseInt(req.params.id, 10)
    res.json(videos[id])
})
Copy the code

The code above takes the ID from the route parameter and converts it to an integer. We then send the object that matches the ID in the videos array back to the client.

Streaming video

In our app.js file, we create a /video route to serve the video to the client. We want this endpoint to be able to send smaller chunks of video rather than serving the entire video file on request.

We want to be able to dynamically provide one of the three videos in the allVideos array and distribute the videos in chunks, so.

Delete /video route from app.js.

We need three videos, so copy the sample video from the tutorial source into assets/ in your Server project. Make sure the file name of the video corresponds to the ID in the Videos array.

Go back to our video.js file and create a route for streaming Video.

router.get('/video/:id', (req, res) => { const videoPath = assets/${req.params.id}.mp4; const videoStat = fs.statSync(videoPath); const fileSize = videoStat.size; const videoRange = req.headers.range; if (videoRange) { const parts = videoRange.replace(/bytes=/, "").split("-"); const start = parseInt(parts[0], 10); const end = parts[1] ? parseInt(parts[1], 10) : fileSize-1; const chunksize = (end-start) + 1; const file = fs.createReadStream(videoPath, {start, end}); const head = { 'Content-Range': bytes ${start}-${end}/${fileSize}, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': 'video/mp4', }; res.writeHead(206, head); file.pipe(res); } else { const head = { 'Content-Length': fileSize, 'Content-Type': 'video/mp4', }; res.writeHead(200, head); fs.createReadStream(videoPath).pipe(res); }});Copy the code

If we navigate to http://localhost:5000/videos/video/outside-the-wire in your browser, we can see the video streaming.

How does streaming video routing work

There’s quite a bit of code in our streaming video routing, so let’s take a look at it line by line.

 const videoPath = `assets/${req.params.id}.mp4`;
 const videoStat = fs.statSync(videoPath);
 const fileSize = videoStat.size;
 const videoRange = req.headers.range;
Copy the code

First, from our request, we use req.params.id, get the ID from the route, and use it to generate the videoPath for the video. Then we read the fileSize using our imported file system fs. For video, the user’s browser will send a range parameter in the request. This lets the server know which piece of the video to send back to the client.

Some browsers send an _ range _ on the initial request, but others do not. For those browsers that don’t send a range, or for any other reason the browser doesn’t send a range, we’ll do else. This code gets the file size and sends the first few blocks of the video.

else {
    const head = {
        'Content-Length': fileSize,
        'Content-Type': 'video/mp4',
    };
    res.writeHead(200, head);
    fs.createReadStream(path).pipe(res);
}
Copy the code

We will process subsequent requests, including scopes, in the if block.

if (videoRange) {
        const parts = videoRange.replace(/bytes=/, "").split("-");
        const start = parseInt(parts[0], 10);
        const end = parts[1]
            ? parseInt(parts[1], 10)
            : fileSize-1;
        const chunksize = (end-start) + 1;
        const file = fs.createReadStream(videoPath, {start, end});
        const head = {
            'Content-Range': bytes ${start}-${end}/${fileSize},
            'Accept-Ranges': 'bytes',
            'Content-Length': chunksize,
            'Content-Type': 'video/mp4',
        };
        res.writeHead(206, head);
        file.pipe(res);
    }
Copy the code

The above code creates a read stream using the start and end values of the range. Set the content-Length of the response header to the block size calculated from the start and end values. We also use HTTP code 206 to indicate that the response contains partial content. This means that the browser will continue making requests until it gets all the blocks of the video.

What happens on unstable connections

If the user’s connection is slow, the network flow signals that the I/O source is paused until the client is ready for more data. This is known as _ back pressure _. We can take this example a step further and see how easy it is to extend the flow. We can also easily add compression.

const start = parseInt(parts[0], 10);
        const end = parts[1]
            ? parseInt(parts[1], 10)
            : fileSize-1;
        const chunksize = (end-start) + 1;
        const file = fs.createReadStream(videoPath, {start, end});
Copy the code

As you can see, a ReadStream is created above and serves the video block by block.

const head = {
            'Content-Range': bytes ${start}-${end}/${fileSize},
            'Accept-Ranges': 'bytes',
            'Content-Length': chunksize,
            'Content-Type': 'video/mp4',
        };
res.writeHead(206, head);
        file.pipe(res);
Copy the code

The request header contains the Content-range, which is the start and end of the changes to get the next entire block of video streaming to the front end, and the Content-Length, which is the entire block of video sent. We also specify the type of content we are streaming, mp4. The write header of 206 is set to respond only to the newly created stream.

Create a title file for our video

This is what a.vTT subtitle file looks like.

WEBVTT

00:00:00.200 --> 00:00:01.000
Creating a tutorial can be very

00:00:01.500 --> 00:00:04.300
fun to do.
Copy the code

The subtitle file contains the words spoken in the video. It also contains the time code that each line of text should display. We want our videos to have subtitles, and we won’t be creating our own subtitles for this tutorial, so you can download the subtitles in the Assets folder in repo.

Let’s create a new route to handle the caption request.

router.get('/video/:id/caption', (req, res) => res.sendFile(assets/captions/${req.params.id}.vtt, { root: __dirname }));
Copy the code

Build our front end

In order to start the visual part of our system, we had to build our front stand.

Note: You will need vuE-CLI to create our application. If you don’t have it installed on your computer, you can run NPM install -g@vue /cli to install it.

The installation

At the root of our project, let’s create our front-end folder.

mkdir frontend
cd frontend
Copy the code

And initialize our package.json file in it, where we copy and paste the following.

{
  "name": "my-app",
  "scripts": {
    "dev": "nuxt",
    "build": "nuxt build",
    "generate": "nuxt generate",
    "start": "nuxt start"
  }
}
Copy the code

Then install nuXT.

npm add nuxt
Copy the code

And execute the following command to run the nuxt.js application.

npm run dev
Copy the code

Our Nuxt file structure

Now that we have Nuxt installed, we can start laying out our front end.

First, we need to create a layouts folder at the root of our application. This folder defines the layout of the application, no matter which page we navigate to. Like our navigation bar and footer right here. In the front-end folder, when we launch our front-end application, we create default.vue for our default layout.

mkdir layouts
cd layouts
touch default.vue
Copy the code

Then create a Components folder to create all of our components. We will only need two components, the NavBar and video components. So in our front-end root directory, we.

mkdir components
cd components
touch NavBar.vue
touch Video.vue
Copy the code

Finally, a Pages folder, where we can create all of our pages, like Home and About. In this application, the two pages we need are a Home page that displays all of our videos and video information, and a dynamic player page that guides the videos we click on.

mkdir pages
cd pages
touch index.vue
mkdir player
cd player
touch _name.vue
Copy the code

Our foreground directory now looks like this.

|-frontend
  |-components
    |-NavBar.vue
    |-Video.vue
  |-layouts
    |-default.vue
  |-pages
    |-index.vue
    |-player
      |-_name.vue
  |-package.json
  |-yarn.lock
Copy the code

Navigation bar component

Our navbar.vue looks like this.

<template>
    <div class="navbar">
        <h1>Streaming App</h1>
    </div>
</template>
<style scoped>
.navbar {
    display: flex;
    background-color: #161616;
    justify-content: center;
    align-items: center;
}
h1{
    color:#a33327;
}
</style>
Copy the code

NavBar has an H1 tag that shows Streaming App with some small styles.

Let’s import NavBar into our default.vue layout.

// default.vue
<template>
 <div>
   <NavBar />
   <nuxt />
 </div>
</template>
<script>
import NavBar from "@/components/NavBar.vue"
export default {
    components: {
        NavBar,
    }
}
</script>
Copy the code

The default.vue layout now contains our NavBar component, followed by a
tag that indicates where any pages we create will be displayed.

In our index. Vue (that is, our home page), let’s request to http://localhost:5000/videos and get all the video from our server. Pass the data as props to the Video.vue component we’ll create later. But now, we’ve imported it.

<template> <div> <Video :videoList="videos"/> </div> </template> <script> import Video from "@/components/Video.vue" export default { components: { Video }, head: { title: "Home" }, data() { return { videos: []}}, async fetch() { this.videos = await fetch( 'http://localhost:5000/videos' ).then(res => res.json()) } } </script>Copy the code

Video component

Next, we declare our props first. Since the video data is now available in the component, using Vue’s V-for, we iterate over all the received data, and for each data, we display information. We can use the V-for directive to loop through the data and display it as a list. Some basic styles have also been added.

<template>
<div>
  <div class="container">
    <div
    v-for="(video, id) in videoList"
    :key="id"
    class="vid-con"
  >
    <NuxtLink :to="`/player/${video.id}`">
    <div
      :style="{
        backgroundImage: `url(${video.poster})`
      }"
      class="vid"
    ></div>
    <div class="movie-info">
      <div class="details">
      <h2>{{video.name}}</h2>
      <p>{{video.duration}}</p>
      </div>
    </div>
  </NuxtLink>
  </div>
  </div>
</div>
</template>
<script>
export default {
    props:['videoList'],
}
</script>
<style scoped>
.container {
  display: flex;
  justify-content: center;
  align-items: center;
  margin-top: 2rem;
}
.vid-con {
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  justify-content: center;
  width: 50%;
  max-width: 16rem;
  margin: auto 2em;

}
.vid {
  height: 15rem;
  width: 100%;
  background-position: center;
  background-size: cover;
}
.movie-info {
  background: black;
  color: white;
  width: 100%;
}
.details {
  padding: 16px 20px;
}
</style>
Copy the code

We also notice that NuxtLink has a dynamic route to /player/video.id.

What we want is a feature that starts playing when the user clicks on any video. To achieve this goal, we take advantage of the dynamic nature of the NAMe.vue route.

In it, we create a video player and set the source as our endpoint to play the video, but we dynamically append the playing video to our endpoint with the help of this.$route.params.name, which parameters the link receives.

<template>
    <div class="player">
        <video controls muted autoPlay>
            <source :src="http://localhost:5000/videos/video/${vidName}" type="video/mp4">
        </video>
    </div>
</template>
<script>
export default {
 data() {
      return {
        vidName: ''
      }
    },
mounted(){
    this.vidName = this.$route.params.name
}
}
</script>
<style scoped>
.player {
    display: flex;
    justify-content: center;
    align-items: center;
    margin-top: 2em;
}
</style>
Copy the code

When we click on any of the videos, we get.

Add our subtitle file

To add our track files, we need to make sure that all the.vtt files in the _ subtitle _ folder match our ID. Update our video elements with tracks and request captions.

<template>
    <div class="player">
        <video controls muted autoPlay crossOrigin="anonymous">
            <source :src="http://localhost:5000/videos/video/${vidName}" type="video/mp4">
            <track label="English" kind="captions" srcLang="en" :src="http://localhost:5000/videos/video/${vidName}/caption" default>
        </video>
    </div>
</template>
Copy the code

We have added crossOrigin=”anonymous” to the video element; Otherwise, the request for subtitles will fail. Now refresh and you will see that the subtitles have been added successfully.

Considerations when building elastic video streams.

There are many things to consider when building a streaming app like Twitch, Hulu or Netflix.

  • Video data processing pipeline This can be a technical challenge, as high-performance servers are required to serve millions of videos to users. High latency or downtime should be avoided at all costs.
  • Caching When building such applications, use caching mechanisms, such as Cassandra, Amazon S3, and AWS SimpleDB.
  • Geographical location of users Considering the geographical location of your users, you should consider distribution.

conclusion

In this tutorial, you’ve seen how to create a server in Node.js to stream videos, generate captions for those videos, and provide metadata for the videos. We also saw how nuxt.js can be used on the front end to consume endpoints and data generated by the server.

Unlike other frameworks, building an application with Nuxt.js and express.js is fairly easy and fast. The cool thing about Nuxt.js is that it manages your routing, allowing you to better structure your application.

  • You can get more information about nuxt.js here.
  • You can get the source code on Github.

resources

  • “Adding subtitles and subtitles to HTML5 video,” MDN Web Document
  • “Understand subtitles and subtitles,” Screenfont. Ca