HLS video in Node.js

HLS video in Node.js

In the process of creating a platform for online video courses I faced a problem with video hosting and streaming. It's one thing when video is hosted by high-end servers at Data Center, but my servers are not so fast and powerful. So I had to find a solution.

Then I decided to use HLS (HTTP Live Streaming). And if you didn't know (as me) what this thing is:

In short, HLS is a media streaming protocol for delivering visual and audio media to viewers over the internet. It divides source file into several smaller chunks

Of course, you could just upload an mp4 file, but HLS gives us an opportunity to provide less resource-intensive video streaming. It will be easier for the server to serve small segments than to serve huge mp4 videos.

But first, we need to somehow receive video, so we need to make sure that the server can receive video from the client. Further actions will be performed on the example of express server with multer. Here's a small configuration for uploading video via multer:

const multer = require("multer");
const path = require("path");

const upload = multer({
    dest: path.join(__dirname, "files", "videos"), //file will be uploaded here
    limits: {
        fileSize: 320 * 1024 * 1024 
    },
    fileFilter: (req, file, callback) => {
        const ext = path.extname(file.originalname);
        const allowed = [".mov", ".avi", ".mp4", ".flv"]; //Filter
        if (allowed.indexOf(ext.toLocaleLowerCase()) < 0) {
            return callback("Only videos are allowed");
        }
        return callback(null, true);
    }
});

module.exports = upload;

Next, we have to create an endpoint to which we will upload the video. It will also trigger video processing, and to make it clear, let's define the steps:

  1. Upload (multer does that)
  2. Create a directory (as HLS is a bunch of files).
  3. Covert video (from any format to HLS).
  4. Delete temporary file.

In order to convert any video into HLS I will use ffmpeg. And for easier work with ffmpeg under node.js I will use fluent-ffmpeg. If we will write everything in one endpoint it will look like that:

const express = require("express");
const fs = require('fs');
const path = require('path')
const ffmpeg = require('fluent-ffmpeg');

const app = express();

const upload = require("./videoUpload");

app.post("/video", upload.single("video"), (req, res) => {
    if (req.file) { //Checking file
        const dir = path.join(__dirname, 'static' , `${req.file.filename}`); //Output directory
        if(!fs.existsSync(dir)){ //Checking if directory exists
            fs.mkdirSync(dir); //Creating a directory
        }
        ffmpeg(req.file.path) //Calling ffmpeg
            .size('1280x?') //Resolution
            .videoBitrate(1024) //Bitrate
            .videoCodec('libx264') //Codec
            .audioBitrate('128k') //Audio bitrate
            .audioChannels(2) //Channels
            .addOption('-hls_time', 10) //Length of a segment
            .addOption('-hls_list_size', 0) //Putting every segment into one playlist
            .output(path.join(__dirname,'static', req.file.filename, 'index.m3u8')) //Saving all files
            .on('error', (err) => { //Error listener
                console.log('Ошибка: ' + err.message); //Displaying an error
            })
            .on('end', () => { //Success
                console.log('Convertation is finished');
                fs.unlink(req.file.path, (err) => { //Deleting source video
                    if (err) throw console.log(`${req.file.path} cannot delete`); //Displaying an error
                });
            })
            .run(); 
        res.json({ 
            success: true,
            file: req.file.filename
        })

    } else {
        res.json({
            success: false
        })
    }
})

app.use(express.static("static")) //Hosting static in order to stream files

app.listen(3003, () => {
    console.log("ready")
})

That's all, but this is far from a complete solution, as only the very minimum for its launch is shown. In production, all this should be more flexible and be able to give data about the process, but this is beyond the scope of this article.